I have an STI model with many associations:
class MyModel < ActiveRecord::base
has_many :things
has_many :other_things
# ... a lot of `has_many`
end
Then I add non STI model as nested just to add some specific behaviour to MyModel without extending it directly:
class Nested < MyModel
self.inheritance_column = nil
end
But then my associations don't work. They have my_model_id column because they refer to MyModel and they should refer to Nested as well. But all these has_many's expect to use nested_id column as a foreign key (it depends on class name).
I could've typed inside class Nested:
has_many :things, foreign_key: 'my_model_id'
has_many :other_things, foreign_key: 'my_model_id'
But if it's possible, how to specify the foreign key for all the associations at once in Nested class?
If all your has_many associations are declared on MyModel, you should be fine, or you may benefit from upgrading Rails; the following works perfectly for me in 4.2.4. Rails uses the class that declares has_many to generate the foreign_key, so even when Nested inherits, :things still get looked up by my_model_id.
class MyModel < ActiveRecord::Base
has_many :things
end
class MyA < MyModel
end
class Nested < MyModel
self.inheritance_column = nil
end
class Thing < ActiveRecord::Base
belongs_to :my_model
end
> m = MyModel.create
=> #<MyModel id: 1, type: nil, created_at: "2015-12-22 02:21:16", updated_at: "2015-12-22 02:21:16">
> m.things.create
=> #<ActiveRecord::Associations::CollectionProxy [#<Thing id: 1, my_model_id: 1, created_at: "2015-12-22 02:21:19", updated_at: "2015-12-22 02:21:19">]>
> n = Nested.create
=> #<Nested id: 2, type: nil, created_at: "2015-12-22 02:21:27", updated_at: "2015-12-22 02:21:27">
> n.things.create
=> #<ActiveRecord::Associations::CollectionProxy [#<Thing id: 2, my_model_id: 2, created_at: "2015-12-22 02:21:32", updated_at: "2015-12-22 02:21:32">]>
> n.reload.things
Nested Load (0.2ms) SELECT "my_models".* FROM "my_models" WHERE "my_models"."id" = ? LIMIT 1 [["id", 2]]
Thing Load (0.1ms) SELECT "things".* FROM "things" WHERE "things"."my_model_id" = ? [["my_model_id", 2]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Thing id: 2, my_model_id: 2, created_at: "2015-12-22 02:21:32", updated_at: "2015-12-22 02:21:32">]>
If you're giving Nested its own has_many associations, the code that generates foreign keys is pretty deep and inaccessible. You end up in the HasManyReflection, which either uses the foreign_key (or as) option declared on your has_many or derives it from the class name. There's no obvious way to customize those methods unless you do something inadvisable like override Nested.name.
foreign_key:
def foreign_key
#foreign_key ||= options[:foreign_key] || derive_foreign_key
end
derive_foreign_key:
def derive_foreign_key
if belongs_to?
"#{name}_id"
elsif options[:as]
"#{options[:as]}_id"
else
active_record.name.foreign_key
end
end
So the simplest way to go about this might just be a loop:
class Nested < MyModel
self.inheritance_column = nil
%i[things other_things].each do |association|
has_many association, foreign_key: 'my_model_id'
end
end
Or if you're feeling metaprogrammy, redefine has_many:
class Nested < MyModel
self.inheritance_column = nil
def self.has_many(klass, options={})
options.reverse_merge!(foreign_key: 'my_model_id')
super
end
has_many :things
has_many :other_things
end
My solution might be is not recommended, but it works. Here it is:
class Nested < MyModel
def self.name
MyModel.name
end
end
ActiveRecord will be looking for my_model_id foreign key for all the associations defined or redefined in Nested and MyModel classes.
Related
I essentially have multiple models with a belongs_to association to another model, but I don't want to define all the has_many relationships on the parent model. Is there a way to create and link this association in one call?
class Thing < ActiveRecord::Base
# does not have any has_many or has_one associations defined
end
class Comment < ActiveRecord::Base
belongs_to :thing
end
# in a controller...
thing = #comment.build_thing(thing_params)
if thing.save
# thing was created but #comment.thing_id was not updated
end
if thing.save && #comment.update(thing: thing)
# this works, but requires an extra call to update the comment
else
# now we would have to check which model failed to save
end
Is there a simple way to do this that I am missing?
has_many association wouldn't change anything (unless :inverse_of is set), you have to save comment either way:
# i assume `belongs_to :thing` is optional v
comment = Comment.create #=> #<Comment:0x00007fec1b612088 id: 1, thing_id: nil>
comment.build_thing #=> #<Thing:0x00007ff8ba420788 id: nil>
comment.save
comment.thing_id #=> 1
comment.thing #=> #<Thing:0x00007ff8ba420788 id: 1>
or like this:
Comment.create(thing: Thing.new) #=> #<Comment:0x00007ff8ba35c1d0 id: 2, thing_id: 2>
I currently have a User model
class User < ApplicationRecord
has_one :leases
end
a Lease model
class Lease < ApplicationRecord
belongs_to :tenant, class_name: 'User'
belongs_to :landlord, class_name: 'User'
end
and a Rental model
class Rental < ApplicationRecord
has_one :lease, dependent: :destroy
end
and I only have one user model supporting tenants and landlords.
The problem I'm facing is that a landlord can have multiple leases with many different tenants, but tenants can only have one lease at a time.
I'm somewhat confused on how I should structure this properly. Should I have a has_many relation with the User model and the Lease model instead of the has_one and then just a method say lease on the User model to get the lease for a tenant?
What I would like to have is something like
tenant.lease
and
landlord.leases
Could I just do?
class User < ApplicationRecord
has_one :lease, foreign_key: "tenant_id"
has_many :leases, foreign_key: "landlord_id"
end
this seems to work, but I'm not sure if its the right way to go.
Using STI, you could have the following models
==> user.rb <==
class User < ApplicationRecord
end
==> landlord.rb <==
class Landlord < User
has_many :leases
end
==> tenant.rb <==
class Tenant < User
has_one :lease
end
==> lease.rb <==
class Lease < ApplicationRecord
belongs_to :tenant
belongs_to :landlord
end
Then you can do things like
irb(main):003:0> landlord = Landlord.create(name: 'the landlord')
=> #<Landlord id: 5, name: "the landlord", type: "Landlord", created_at: "2022-01-03 22:16:56", updated_at: "2022-01-03 22:16:56">
irb(main):004:0> tenant = Tenant.create(name: 'the tenant')
=> #<Tenant id: 6, name: "the tenant", type: "Tenant", created_at: "2022-01-03 22:17:09", updated_at: "2022-01-03 22:17:09">
irb(main):005:0> lease = Lease.create(tenant: tenant, landlord: landlord)
=> #<Lease id: 3, tenant_id: 6, landlord_id: 5, created_at: "2022-01-03 22:17:28", updated_at: "2022-01-03 22:17:28">
irb(main):006:0> tenant.lease
=> #<Lease id: 3, tenant_id: 6, landlord_id: 5, created_at: "2022-01-03 22:17:28", updated_at: "2022-01-03 22:17:28">
irb(main):007:0> landlord.leases
=> #<ActiveRecord::Associations::CollectionProxy [#<Lease id: 3, tenant_id: 6, landlord_id: 5, created_at: "2022-01-03 22:17:28", updated_at: "2022-01-03 22:17:28">]>
To accomplish this, you only need to add a type column to your User model and Rails will take care of the rest.
In my opinion, you should go with has_many approach. As landlord and tenant both are users. in place of using tenant_id and landlord_id, you have to use 'user_id' and to identify user, use user_type i.e. tenant and lanlord.
Just an opinion !!
I have the following models and relationships. I'm building a form and am wanting to initialize terms of the proposal for the form. How can I select a specific ProposalTerm by it's term_type_id to pass on to my fields_for block?
Proposal
class Proposal < ApplicationRecord
after_initialize :add_terms
has_many :terms, class_name: "ProposalTerm", dependent: :destroy
accepts_nested_attributes_for :terms
def add_terms
terms << ProposalTerm.first_or_initialize(type: TermType.signing_bonus)
end
end
ProposalTerm
class ProposalTerm < ApplicationRecord
include DisableInheritance
belongs_to :proposal
belongs_to :type, class_name: "TermType", foreign_key: "term_type_id"
def self.signing_bonus
find_by(type: TermType.signing_bonus)
end
end
My Attempt
>> #proposal.terms
=> #<ActiveRecord::Associations::CollectionProxy [#<ProposalTerm id: nil, season: nil, value: nil, is_guaranteed: false, term_type_id: 2, proposal_id: nil, created_at: nil, updated_at: nil>]>
>> #proposal.terms.where(term_type_id: 2)
=> #<ActiveRecord::AssociationRelation []>
I was able to figure out an answer. I had tried "select" but I was doing it incorrectly.
I had tried the following,
#proposal.terms.select(term_type_id: 2)
but that wasn't returning anything. I then did the following...
#proposal.terms.select { |t| t.term_type_id = 2 }
If you want to return just the first instance use "detect" ...
#proposal.terms.detect { |t| t.term_type_id = 2 } }
I've created two models with the below associations
class User < ActiveRecord::Base
has_many :roles, :dependent => :destroy
end
class Role < ActiveRecord::Base
belongs_to :user
end
class Student < Role
end
class Tutor < Role
end
However when I create a new child role, I assume it would get associated to the model it has the belongs to for.
Such as:
Tutor.create(:user_id => user_id)
I would expect:
#some user #user
#user.roles
to have an array containing a Tutor. However, it doesn't seem to be working. Any ideas what I'm doing wrong?
Once you start using Single Table Inheritance, than the Tutor that you created isn't a role, as far as active-record is concerned for this type of query.
class User < ActiveRecord::Base
has_many :roles
has_many :tutors
end
#user = User.first
#user.roles
=> []
#user.tutors
=> [#<Tutor id: 1, user_id: 1, type: "Tutor", created_at: "2012-10-26 18:15:16", updated_at: "2012-10-26 18:15:16">]
If you want to get a list of all roles that your users may have:
Role.where(user_id: #user.id).all
[#<Tutor id: 1, user_id: 1, type: "Tutor", created_at: "2012-10-26 18:15:16", updated_at: "2012-10-26 18:15:16">, #<Student id: 2, user_id: 1, type: "Student", created_at: "2012-10-26 18:18:32", updated_at: "2012-10-26 18:18:32">]
I've came into a problem while working with AR and polymorphic, here's the description,
class Base < ActiveRecord::Base; end
class Subscription < Base
set_table_name :subscriptions
has_many :posts, :as => :subscriptable
end
class Post < ActiveRecord::Base
belongs_to :subscriptable, :polymorphic => true
end
in the console,
>> s = Subscription.create(:name => 'test')
>> s.posts.create(:name => 'foo', :body => 'bar')
and it created a Post like:
#<Post id: 1, name: "foo", body: "bar", subscriptable_type: "Base", subscriptable_id: 1, created_at: "2010-05-10 12:30:10", updated_at: "2010-05-10 12:30:10">
the subscriptable_type is Base but Subscription, anybody can give me a hand on this?
If the class Base is an abstract model, you have to specify that in the model definition:
class Base < ActiveRecord::Base
self.abstract_class = true
end
Does your subscriptions table have a 'type' column? I'm guessing that Rails thinks that Base/Subscription are STI models. So when a row is retrieved from the subscriptions table and no 'type' column is present, it just defaults to the parent class of Base. Just a guess...