I have a somewhat complex Rails model setup that I'll try to simplify as much as possible. The goal of this setup is to be able to have objects (Person, Pet) that are long-lived, but with relationships between them changing each year via TemporalLink. Basically, I have these models:
class Person < ActiveRecord::Base
include TemporalObj
has_many :pet_links, class_name: "PetOwnerLink"
has_many :pets, through: :pet_links
end
class Pet < ActiveRecord::Base
include TemporalObj
has_many :owner_links, class_name: "PetOwnerLink"
has_many :owners, through: :owner_links
end
class PetOwnerLink < ActiveRecord::Base
include TemporalLink
belongs_to :owner
belongs_to :pet
end
and these concerns:
module TemporalLink
extend ActiveSupport::Concern
# Everything that extends TemporalLink must have a `year` attribute.
end
module TemporalObj
extend ActiveSupport::Concern
# Everything that extends TemporalObj must have a find_existing() method.
####################
# Here be dragons! #
####################
end
The desired behavior is:
When creating a TemporalObj (Pet, Person):
1) Check to see if there is an existing one, based on certain conditions, with find_existing().
2) If an existing duplicate is found, don't perform the create but still perform necessary creations to associated objects. (This seems to be the tricky part.)
3) If no duplicate is found, perform the create.
4) [Existing magic already auto-creates the necessary TemporalLink objects.]
When destroying a TemporalObj:
1) Check to see if the object exists in more than one year. (This is simpler in actuality than in this example.)
2) If the object exists in only one year, destroy it and associated TemporalLinks.
3) If the object exists in more than one year, just destroy one of the TemporalLinks.
My problem is I have uniqueness validations on many TemporalObjs, so when I try to create a new duplicate, the validation fails before I can perform any around_create magic. Any thoughts on how I can wrangle this to work?
You can (and should) use Rails' built-in validations here. What you've described is validates_uniqueness_of, which you can scope to include multiple columns.
For example:
class TeacherSchedule < ActiveRecord::Base
validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
end
http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_uniqueness_of
In response to JacobEvelyn's comment, this is what I did.
Created a custom validate like so
def maintain_uniqueness
matching_thing = Thing.find_by(criteria1: self.criteria1, criteria2: self.criteria2)
if !!matching_thing
self.created_at = matching_thing.created_at
matching_thing.delete
end
true
end
Added it to my validations
validate :maintain_event_uniqueness
It worked.
Related
Lately I've grown weary of littering my app/models directory with pointless boilerplate models such as:
Join models that always contain a couple belongs_tos and nothing else.
Status log models that just include SomeConcern and make a couple macro calls.
Revision tracking models that again, just include a concern and call a macro.
These models only exist to support has_many and has_many ... through: associations.
Adding model concerns that generate these models as needed clears simplifies the app/models directory. So instead of:
has_many :model_things
has_many :things, through: :model_things
and a trivial app/models/model_thing.rb that says:
class ModelThing < ApplicationRecord
belongs_to :model
belongs_to :thing
end
I can have a ThingSupport concern with a has_things macro that:
Creates the has_many :model_things association based on the class name and some options to has_things.
Creates the has_many :things, through: :model_things association.
Find or create the Model::Thing (see below for why this name is used) class with a call like:
ModuleUtil.find_or_create(join_model_name) do
Class.new(ApplicationRecord) do
# Set the table name, call belongs_to as needed, call concern methods, ...
end
end
where ModuleUtil.find_or_create is a simple method that uses String#constantize to find the desired module (if it exists) or create it using the block and Object#const_set if it can't be found.
All the model and association names can be built using the usual Rails conventions from the caller's class name and some options to has_things for special cases.
The question is am I playing with fire here? What can go wrong with this sort of chicanery?
One problem that I've already come across is that the model classes that are generated don't exist on their own so they cannot be directly referenced from an ActiveJob (such as a deliver_later mailer). For example, if loading Model creates the ModelThing association model then you can't reference a ModelThing in a mailer argument because ActiveJob won't know that you have to load the Model class before ModelThing exists. However, this can be solved by using Model::Thing instead so that constantize will look for Model (and find it in app/models/model.rb) before trying to find Model::Thing (which will exist because constantize will have just loaded Model which creates Model::Thing). Am I missing something else?
I have no idea if I'm following you or not. So, if this is way off target, please say so and I'll delete.
Focusing in on the join model bit, I also got tired of that flim flam. So, I created a model like:
module ActsAsHaving
class HasA < ActiveRecord::Base
validates :haser_type, :haser_id, :hased_type, :hased_id, presence: true
belongs_to :hased, polymorphic: true
belongs_to :haser, polymorphic: true
acts_as_taggable
def haser=(thing)
self.haser_type = thing.class.name
self.haser_id = thing.id
end
def haser
haser_type.constantize.find_by(id: haser_id)
end
def hased=(thing)
self.hased_type = thing.class.name
self.hased_id = thing.id
end
def hased
hased_type.constantize.find_by(id: hased_id)
end
end
end
I didn't use the built-in accessors and validations because I sometimes use this to join non-AR records (which I grab from remote API services some of which belong to me and some of which don't but that's a longer story).
Anyway, I then wrote an acts_as_having macro that let me do stuff like:
class Person < ActiveRecord::Base
acts_as_having :health_events, class_name: "Foo::Event", tag_with: "health_event", remote: true
acts_as_having :program_events, class_name: "Foo::Event", tag_with: "program_event", remote: true
acts_as_having :email_addresses, :phone_numbers, :physical_addresses
end
Which gives me stuff like:
#person.email_addresses
#person.email_addresses << #email_address
etc...
I can do the inverse like:
class EmailAddress < ActiveRecord::Base
acts_as_had_by :person
end
Which gives me stuff like:
#email_address.person
etc...
Then, I wrapped all that junk up into a gem. Now I rarely create join models unless they have some specific requirements that I can't shoe horn into my acts_as_having bit.
Anyway, I don't know if it's playing with fire or not. I don't even know if I'm making sense or addressing your concept. But, I started my gem about three years ago and I haven't regretted it. So, there's that.
I have a Sponsors model and a Promo Codes model.
A sponsor can have zero or more promo codes
A promo code can have zero or one sponsors
Thus a promo code should have an optional reference to a sponsor, that is, a sponsor_id that may or may not have a value. I'm not sure how to set this up in Rails.
Here's what I have so far:
# app/models/sponsor.rb
class Sponsor < ActiveRecord::Base
has_many :promo_codes # Zero or more.
end
# app/models/promo_code.rb
class PromoCode < ActiveRecord::Base
has_one :sponsor # Zero or one.
end
# db/migrate/xxxxx_add_sponsor_reference_to_promo_codes.rb
# rails g migration AddSponsorReferenceToPromoCodes sponsor:references
# Running migration adds a sponsor_id field to promo_codes table.
class AddSponsorReferenceToPromoCodes < ActiveRecord::Migration
def change
add_reference :promo_codes, :sponsor, index: true
end
end
Does this make sense? I'm under the impression that I have to use belongs_to in my Promo Codes model, but I have no basis for this, just that I've haven't seen a has_many with has_one example yet.
In Rails 5, belongs_to is defined as required by default. To make it optional use the 'optional' option :)
class User
belongs_to :company, optional: true
end
Source: https://github.com/rails/rails/issues/18233
This looks like a simple has_many and belongs_to relationship:
# app/models/sponsor.rb
class Sponsor < ActiveRecord::Base
has_many :promo_codes # Zero or more.
end
# app/models/promo_code.rb
#table has sponsor_id field
class PromoCode < ActiveRecord::Base
belongs_to :sponsor # Zero or one.
end
has_one isn't appropriate here, as it would replace has_many: ie, you either have "has_many" and "belongs_to" OR "has_one" and "belongs_to". has_one isn't generally used much: usually it is used when you already have a has_many relationship that you want to change to has_one, and don't want to restructure the existing tables.
Unless you specify validation, relationships are optional by default.
The belongs_to is to tell rails the other half of the relationship between those two objects so you can also call #promo_code.sponsor and, vice versa, #sponsor.promo_codes.
Relationships
class Promotion < ActiveRecord::Base
has_many :promotion_sweepstakes,
has_many :sweepstakes,
:through => :promotion_sweepstakes
end
class PromotionSweepstake < ActiveRecord::Base
belongs_to :promotion
belongs_to :sweepstake
end
class Sweepstake < ActiveRecord::Base
# Not relevant in this question, but I included the class
end
So a Promotion has_many Sweepstake through join table PromotionSweepstake. This is a legacy db schema so the naming might seem a bit odd and there are some self.table_name == and foreign_key stuff left out.
The nature of this app demands that at least one entry in the join table is present for a promotionId, because not having a sweepstake would break the app.
First question
How can I guarantee that there is always one entry in PromotionSweepstake for a Promotion? At least one Sweepstake.id has to be included upon creation, and once an entry in the join table is created there has to be a minimum of one for each Promotion/promotion_id.
Second question (other option)
If the previous suggestion would not be possible, which I doubt is true, there's another way the problem can be worked around. There's a sort of "default Sweepstake" with a certain id. If through a form all the sweepstake_ids would be removed (so that all entries for the Promotion in the join table would be deleted), can I create a new entry in PromotionSweepstake?
pseudo_code (sort of)
delete promotion_sweepstake with ids [1, 4, 5] where promotion_id = 1
if promotion with id=1 has no promotion_sweepstakes
add promotion_sweepstake with promotion_id 1 and sweepstake_id 100
end
Thank you for your help.
A presence validation should solve the problem in case of creation and modification of Promotions.
class Promotion < ActiveRecord::Base
has_many :promotion_sweepstakes
has_many :sweepstakes,
:through => :promotion_sweepstakes
validates :sweepstakes, :presence => true
end
In order to assure consistency when there's an attempt to delete or update a Sweepstake or a PromotionSweepstake you'd have to write your own validations for those two classes. They would have to check whether previously referenced Promotions are still valid, i.e. still have some Sweepstakes.
A simple solution would take and advantage of validates :sweepstakes, :presence => true in Promotion. After updating referenced PromotionSweepstakes or Sweepstakes in a transaction you would have to call Promotion#valid? on previously referenced Promotions. If they're not valid you roll back the transaction as the modification broke the consistency.
Alternatively you could use before_destroy in both PromotionSweepstake and Sweepstake in order to prevent changes violating your consistency requirements.
class PromotionSweepstake < ActiveRecord::Base
belongs_to :promotion
belongs_to :sweepstake
before_destroy :check_for_promotion_on_destroy
private
def check_for_promotion_on_destroy
raise 'deleting the last sweepstake' if promotion.sweepstakes.count == 1
end
end
class Sweepstake < ActiveRecord::Base
has_many :promotion_sweepstakes
has_many :promotions, :through => :promotion_sweepstakes
before_destroy :check_for_promotions_on_destroy
private
def check_for_promotions_on_destroy
promotions.each do |prom|
raise 'deleting the last sweepstake' if prom.sweepstakes.count == 1
end
end
end
I have a case of polymorphic association and STI here.
# app/models/car.rb
class Car < ActiveRecord::Base
belongs_to :borrowable, :polymorphic => true
end
# app/models/staff.rb
class Staff < ActiveRecord::Base
has_one :car, :as => :borrowable, :dependent => :destroy
end
# app/models/guard.rb
class Guard < Staff
end
In order for the polymorphic assocation to work, according to the API documentation on Polymorphic Assocation, http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations that I have to set borrowable_type to the base_classof STI models, that is in my case is Staff.
The question is: Why doesn't it work if the borrowable_type set to STI class?
Some test to prove it:
# now the test speaks only truth
# test/fixtures/cars.yml
one:
name: Enzo
borrowable: staff (Staff)
two:
name: Mustang
borrowable: guard (Guard)
# test/fixtures/staffs.yml
staff:
name: Jullia Gillard
guard:
name: Joni Bravo
type: Guard
# test/units/car_test.rb
require 'test_helper'
class CarTest < ActiveSupport::TestCase
setup do
#staff = staffs(:staff)
#guard = staffs(:guard)
end
test "should be destroyed if an associated staff is destroyed" do
assert_difference('Car.count', -1) do
#staff.destroy
end
end
test "should be destroyed if an associated guard is destroyed" do
assert_difference('Car.count', -1) do
#guard.destroy
end
end
end
But it seems to be true only with Staff instance. The results are:
# Running tests:
F.
Finished tests in 0.146657s, 13.6373 tests/s, 13.6373 assertions/s.
1) Failure:
test_should_be_destroyed_if_an_associated_guard_is_destroyed(CarTest) [/private/tmp/guineapig/test/unit/car_test.rb:16]:
"Car.count" didn't change by -1.
<1> expected but was
<2>.
Thanks
Good question. I had exactly the same problem using Rails 3.1. Looks like you can not do this, because it does not work. Probably it is an intended behavior. Apparently, using polymorphic associations in combination with Single Table Inheritance (STI) in Rails is a bit complicated.
The current Rails documentation for Rails 3.2 gives this advice for combining polymorphic associations and STI:
Using polymorphic associations in combination with single table
inheritance (STI) is a little tricky. In order for the associations to
work as expected, ensure that you store the base model for the STI
models in the type column of the polymorphic association.
In your case the base model would be "Staff", i.e. "borrowable_type" should be "Staff" for all items, not "Guard". It is possible to make the derived class appear as the base class by using "becomes" : guard.becomes(Staff). One could set the column "borrowable_type" directly to the base class "Staff", or as the Rails Documentation suggests, convert it automatically using
class Car < ActiveRecord::Base
..
def borrowable_type=(sType)
super(sType.to_s.classify.constantize.base_class.to_s)
end
An older question, but the issue in Rails 4 still remains. Another option is to dynamically create/overwrite the _type method with a concern. This would be useful if your app uses multiple polymorphic associations with STI and you want to keep the logic in one place.
This concern will grab all polymorphic associations and ensure that the record is always saved using the base class.
# models/concerns/single_table_polymorphic.rb
module SingleTablePolymorphic
extend ActiveSupport::Concern
included do
self.reflect_on_all_associations.select{|a| a.options[:polymorphic]}.map(&:name).each do |name|
define_method "#{name.to_s}_type=" do |class_name|
super(class_name.constantize.base_class.name)
end
end
end
end
Then just include it in your model:
class Car < ActiveRecord::Base
belongs_to :borrowable, :polymorphic => true
include SingleTablePolymorphic
end
Just had this issue in Rails 4.2. I found two ways to resolve:
--
The problem is that Rails uses the base_class name of the STI relationship.
The reason for this has been documented in the other answers, but the gist is that the core team seem to feel that you should be able to reference the table rather than the class for a polymorphic STI association.
I disagree with this idea, but am not part of the Rails Core team, so don't have much input into resolving it.
There are two ways to fix it:
--
1) Insert at model-level:
class Association < ActiveRecord::Base
belongs_to :associatiable, polymorphic: true
belongs_to :associated, polymorphic: true
before_validation :set_type
def set_type
self.associated_type = associated.class.name
end
end
This will change the {x}_type record before the creation of the data into the db. This works very well, and still retains the polymorphic nature of the association.
2) Override Core ActiveRecord methods
#app/config/initializers/sti_base.rb
require "active_record"
require "active_record_extension"
ActiveRecord::Base.store_base_sti_class = false
#lib/active_record_extension.rb
module ActiveRecordExtension #-> http://stackoverflow.com/questions/2328984/rails-extending-activerecordbase
extend ActiveSupport::Concern
included do
class_attribute :store_base_sti_class
self.store_base_sti_class = true
end
end
# include the extension
ActiveRecord::Base.send(:include, ActiveRecordExtension)
####
module AddPolymorphic
extend ActiveSupport::Concern
included do #-> http://stackoverflow.com/questions/28214874/overriding-methods-in-an-activesupportconcern-module-which-are-defined-by-a-cl
define_method :replace_keys do |record=nil|
super(record)
owner[reflection.foreign_type] = ActiveRecord::Base.store_base_sti_class ? record.class.base_class.name : record.class.name
end
end
end
ActiveRecord::Associations::BelongsToPolymorphicAssociation.send(:include, AddPolymorphic)
A more systemic way to fix the issue is to edit the ActiveRecord core methods which govern it. I used references in this gem to find out which elements needed to be fixed / overridden.
This is untested and still needs extensions for some of the other parts of the ActiveRecord core methods, but seems to work for my local system.
There is a gem.
https://github.com/appfolio/store_base_sti_class
Tested and it works on various versions of AR.
You can also build a custom scope for a has_* association for the polymorphic type:
class Staff < ActiveRecord::Base
has_one :car,
->(s) { where(cars: { borrowable_type: s.class }, # defaults to base_class
foreign_key: :borrowable_id,
:dependent => :destroy
end
Since polymorphic joins use a composite foreign key (*_id and *_type) you need to specify the type clause with the correct value. The _id though should work with just the foreign_key declaration specifying the name of the polymorphic association.
Because of the nature of polymorphism it can be frustrating to know what models are borrowables, since it could conceivably be any model in your Rails application. This relationship will need to be declared in any model where you want the cascade deletion on borrowable to be enforced.
This is how I solved that problem using aforementioned hints:
# app/models/concerns/belongs_to_single_table_polymorphic.rb
module BelongsToSingleTablePolymorphic
extend ActiveSupport::Concern
included do
def self.belongs_to_sti_polymorphic(model)
class_eval "belongs_to :#{model}, polymorphic: true"
class_eval 'before_validation :set_sti_object_type'
define_method('set_sti_object_type') do
sti_type = send(model).class.name
send("#{model}_type=", sti_type)
end
end
end
end
and with that, for any model in which I would find belongs_to :whatever, polymorphic: true I do:
class Reservation < ActiveRecord::Base
include BelongsToSingleTablePolymorphic
# .....
belongs_to_sti_polymorphic :whatever
# .....
end
I agree with the general comments that this ought to be easier. That said, here is what worked for me.
I have a model with Firm as the base class and Customer and Prospect as the STI classes, as so:
class Firm
end
class Customer < Firm
end
class Prospect < Firm
end
I also have a polymorphic class, Opportunity, which looks like this:
class Opportunity
belongs_to :opportunistic, polymorphic: true
end
I want to refer to opportunities as either
customer.opportunities
or
prospect.opportunities
To do that I changed the models as follows.
class Firm
has_many opportunities, as: :opportunistic
end
class Opportunity
belongs_to :customer, class_name: 'Firm', foreign_key: :opportunistic_id
belongs_to :prospect, class_name: 'Firm', foreign_key: :opportunistic_id
end
I save opportunities with an opportunistic_type of 'Firm' (the base class) and the respective customer or prospect id as the opportunistic_id.
Now I can get customer.opportunities and prospect.opportunities exactly as I want.
In my Rails 3.1 project, I have some models with lots of associations. Using ActiveRecord association declarations, I end up with model files that look like this:
# app/models/some_model.rb
class SomeModel < ActiveRecord::Base
belongs_to :other_model
has_many :more_models
has_many :yet_more_models, :through => :more_models
has_one :another_model, :dependent => :destroy
# ... these declarations continue,
# and continue,
# and continue,
# all the way down to line 32
end
This quickly becomes exceedingly ugly and dampens my comprehension/motivation/happiness. What can I do to mitigate?
[a] Format/group/indent them in a particular way?
[b] Re-think my data model, as this may be a symptom of poor design
[c] Live with it -- everyone's model files look this way.
is it possible to group them, by different aspects / functionality of your SomeModel ? do these group of associations tend to have quite a lot of accompanying methods in your SomeModel class? if so, dividing your model into a few modules (like traits), one for every aspect, bundling everything including class methods and association declarations, may help.
e.g.
class SomeModel
include SomeModel::ThisBehavior
include SomeModel::ThatFeature
end
and
module SomeModel::ThisBehavior
extend ActiveSupport::Concern
included do
has_many :this
has_many :that
belongs_to :those
attr_protected :a, :b
attr_accessor :c, :d
end
def do_this
end
...
module ClassMethods
...
end
end
The next step could be trying to make those modules quite agnostic, and group your tests accordingly.
The general rule of thumb is to align related assignments vertically. That carries through to related declarations too.
class SomeModel < ActiveRecord::Base
belongs_to :other_model
has_many :more_models
has_many :yet_more_models, :through => :more_models
has_one :another_model, :dependent => :destroy
end
If you think this is verbose, you haven't seen DataMapper models :P
You can have a model with lots of associations and it's fine for me. If there is a complex logic behind, it will result on a complex bunch of associations. For example, I have an Account class that has over 60 associations: users, companies, centers, products, documents, routes, vehicles .....
This question is more about readability. First of all, decide a convention and follow the same rule all over the project (belongs_to first, has_one second, has_many third, habtm last)
Second advice: If some relations are clearly related with a well separated functionality, you can split your class into some modules keeping each relation in the modules it concerns. But this is a general rule.
class Account < ActiveRecord::Base
include Account::CRM
include Account::Plans
include Account::Finances
end
Maybe you can distribute parent models into others...
Example, I have an app that uses three different instances of User:
class User < ActiveRecord::Base
has_one :social_profile
has_one :tasks_profile
has_one :bank_account
end
And other models that represents user in other project scopes:
class SocialProfile < ActiveRecord::Base
belongs_to :user
has_many :many_things
...
end
Same for TasksProfile and BankAccount.