I have a polymorphic relation as follows:
class Profile
belongs_to :practice, polymorphic: :true
end
class ForeclosurePractice
has_one :profile, as: :practice
end
I want to build a practice object based on the profile I have, but unfortunately practice returns nil:
p = Profile.new
p.practice # => nil
How can I build the practice object from the Profile object?
p.build_practice won't work, because the build_other method is not generated for polymorphic associations.
If you want a way to dynamically create an instance, for example based on a class name selected in a form, you can try to use safe_constantize - simple example:
p.practice = params[:practice_type].safe_constantize.new
Building on Matt's answer I would adjust the build_practice method like this:
I ran into the issue that updating the polymorphic practice would create a new object instead. (Because build_practice always calls. new.
class Profile < ApplicationRecord
def build_practice(params)
if practice.present?
practice.assign_attributes(params)
else
self.practice = practice_type.classify.constantize.new(params)
end
end
First, I was only using the second part and would wonder that it creates a new record with every update.
You need to explicitly build the association:
p = Profile.new
p.build_practice
See: http://apidock.com/rails/v4.0.2/ActiveRecord/Associations/ClassMethods/belongs_to
Related
Background
I have some polymorphic relationships - one in particular assetable where I have a parent-child Asset relationship and the parent has various classes.
I also have a global Tracker model that creates a global id type scheme across my various models which I also use the FriendlyID gem for.
My Goal
Something like this:
parent = Tracker.find_by(tracker: '4Q73XEGK').trackable
Asset.find(1).update(parent_tracker: parent.tracker_id)
thinking I could do something like this - set the new polymorphic relationship by the tracker_id:
class Asset < ActiveRecord::Base
def parent_tracker
assetable.tracker_id
end
def parent_tracker=(val)
a = Tracker.find_by(tracker: val).trackable
assetable_id = a.id
assetable_type = a.class.name
end
end
Question?
Am I on the right path here (with tweaks) OR show that I am on the completely wrong path here.
I know that there are before_save filters etc. but the setter approach seems more elegant and clear as I can apply this across many other models.
You should not have to set both the type and id - use the setter created by the association instead:
class Asset < ActiveRecord::Base
belongs_to :assetable, polymorphic: true
def parent_tracker
assetable.tracker_id
end
def parent_tracker=(val)
self.assetable = Tracker.find_by(tracker: val).trackable
end
end
The setter for polymorphic associations will set both the id and type attributes. Also note that you need to use self explicitly when calling setters.
assetable_id = a.id
assetable_type = a.class.name
Will just set local variables that are garbage collected when the method ends.
Tracker.find_by(tracker: val) feels really smelly too. If your Tracker class just keeps track of global ids shouldn't it provide a method that takes such an id and returns the trackable?
class Tracker < ActiveRecord::Base
def self.lookup(global_id)
find_by(tracker: global_id).trackable
end
end
class Asset < ActiveRecord::Base
belongs_to :assetable, polymorphic: true
# ...
def parent_tracker=(val)
self.assetable = Tracker.lookup(val)
end
end
I have a has_may through association and I'm trying to change records in the association in memory and then have all the associations updated in a single transaction on #save. I can't figure out how to make this work.
Here's a simplifiction of what I'm doing (using the popular Blog example):
# The model
class Users < ActiveRecord::Base
has_many :posts, through: user_posts
accepts_nested_attributes_for :posts
end
# The controller
class UsersController < ApplicationController
def update
user.assign_attributes(user_params)
replace_existing_posts(user)
user.save
end
private
def replace_existing_posts(user)
user.posts.each do |post|
existing = Post.find_by(title: post.title)
next unless existing
post.id = existing
post.reload
end
end
end
This is a bit contrived. The point is that if a post that the user added already exists in the system, we just assign the existing post to them. If the post does not already exist we create a new one.
The problem is, that when I call user.save it saves any new posts (and the user_post association) but doesn't create the user_post association for the existing record.
I've tried to resolve this by adding has_many :user_posts, autosave: true to the User model, but despite the documented statement "When :autosave is true all children are saved", that doesn't reflect the behavior I see.
I can make this work, with something hacky like this, but I don't want to save the association records separately (and removing and replacing all associations would lead to lots of callback I don't want to fire).
posts = user.posts.to_a
user.posts.reset
user.posts.replace(posts)
I've trolled through the ActiveRecord docs and the source code and haven't found a way to add records to a has_many through association that create the mapping record in memory.
I finally got this to work, just by adding the association records manually.
So now my controller also does this in the update:
user.posts.each do |post|
next unless post.persisted?
user.user_posts.build(post: post)
end
Posting this as an answer unless someone has a better solution.
So I've got a User model, a Building model, and a MaintenanceRequest model.
A user has_many :maintenance_requests, but belongs_to :building.
A maintenance requests belongs_to :building, and belongs_to: user
I'm trying to figure out how to send a new, then create a maintenance request.
What I'd like to do is:
#maintenance_request = current_user.building.maintenance_requests.build(permitted_mr_params)
=> #<MaintenanceRequest id: nil, user_id: 1, building_id: 1>
And have a new maintenance request with the user and building set to it's parent associations.
What I have to do:
#maintenance_request = current_user.maintenance_requests.build(permitted_mr_params)
#maintenance_request.building = current_user.building
It would be nice if I could get the maintenance request to set its building based of the user's building.
Obviously, I can work around this, but I'd really appreciate the syntactic sugar.
From the has_many doc
You can pass a second argument scope as a callable (i.e. proc or lambda) to retrieve a specific set of records or customize the generated query when you access the associated collection.
I.e
class User < ActiveRecord::Base
has_many :maintenance_requests, ->(user){building: user.building}, through: :users
end
Then your desired one line should "just work" current_user.building.maintenance_requests.build(permitted_mr_params)
Alternatively, if you are using cancancan you can add hash conditions in your ability file
can :create, MaintenanceRequest, user: #user.id, building: #user.building_id
In my opinion, I think the approach you propose is fine. It's one extra line of code, but doesn't really increase the complexity of your controller.
Another option is to merge the user_id and building_id, in your request params:
permitted_mr_params.merge(user_id: current_user.id, building_id: current_user.building_id)
#maintenance_request = MaintenanceRequest.create(permitted_mr_params)
Or, if you're not concerned about mass-assignment, set user_id and building_id as a hidden field in your form. I don't see a tremendous benefit, however, as you'll have to whitelist the params.
My approach would be to skip
maintenance_request belongs_to :building
since it already belongs to it through the user. Instead, you can define a method
class MaintenanceRequest
belongs_to :user
def building
user.building
end
#more class stuff
end
Also, in building class
class Building
has_many :users
has_many :maintenance_requests, through: :users
#more stuff
end
So you can completely omit explicit building association with maintenance_request
UPDATE
Since users can move across buildings, you can set automatic behavior with a callback. The job will be done like you do it, but in a more Railsey way
class MaintenanceRequest
#stuff
before_create {
building=user.building
}
end
So, when you create the maintenance_request for the user, the building will be set accordingly
This post seems good for how to create two models with one form. But how would you do it if the two models share one or more of the attributes?
That post seems fairly outdated, I would recommend using accepts_nested_attributes_for and fields_for in your form instead. That said, overlapping attributes should probably be set in your model's callbacks. Say you want a project's name to be automatically set to first task's name.
class Project < ActiveRecord::Base
has_many :tasks
accepts_nested_attributes_for :tasks
before_validation :set_name_from_task
private
def set_name_from_task
self.name = tasks.first.name
end
end
If your 2 models are completely unrelated, you can assign certain params to them directly in the controller.
def create
#foo = Foo.new(params[:foo])
#bar = Bar.new(params[:bar])
#bar.common_attr = params[:foo][:common_attr]
# validation/saving logic
end
Although this is not a great practice, this logic should ideally be moved into models.
What's the best practice to create has_one relations?
For example, if I have a user model, and it must have a profile...
How could I accomplish that?
One solution would be:
# user.rb
class User << ActiveRecord::Base
after_create :set_default_association
def set_default_association
self.create_profile
end
end
But that doesn't seem very clean... Any suggestions?
Best practice to create has_one relation is to use the ActiveRecord callback before_create rather than after_create. Or use an even earlier callback and deal with the issues (if any) of the child not passing its own validation step.
Because:
with good coding, you have the opportunity for the child record's validations to be shown to the user if the validations fail
it's cleaner and explicitly supported by ActiveRecord -- AR automagically fills in the foreign key in the child record after it saves the parent record (on create). AR then saves the child record as part of creating the parent record.
How to do it:
# in your User model...
has_one :profile
before_create :build_default_profile
private
def build_default_profile
# build default profile instance. Will use default params.
# The foreign key to the owning User model is set automatically
build_profile
true # Always return true in callbacks as the normal 'continue' state
# Assumes that the default_profile can **always** be created.
# or
# Check the validation of the profile. If it is not valid, then
# return false from the callback. Best to use a before_validation
# if doing this. View code should check the errors of the child.
# Or add the child's errors to the User model's error array of the :base
# error item
end
Your solution is definitely a decent way to do it (at least until you outgrow it), but you can simplify it:
# user.rb
class User < ActiveRecord::Base
has_one :profile
after_create :create_profile
end
If this is a new association in an existing large database, I'll manage the transition like this:
class User < ActiveRecord::Base
has_one :profile
before_create :build_associations
def profile
super || build_profile(avatar: "anon.jpg")
end
private
def build_associations
profile || true
end
end
so that existing user records gain a profile when asked for it and new ones are created with it. This also places the default attributes in one place and works correctly with accepts_nested_attributes_for in Rails 4 onwards.
Probably not the cleanest solution, but we already had a database with half a million records, some of which already had the 'Profile' model created, and some of which didn't. We went with this approach, which guarantees a Profile model is present at any point, without needing to go through and retroactively generate all the Profile models.
alias_method :db_profile, :profile
def profile
self.profile = Profile.create(:user => self) if self.db_profile.nil?
self.db_profile
end
Here's how I do it. Not sure how standard this is, but it works very well and its lazy in that it doesn't create extra overhead unless it's necessary to build the new association (I'm happy to be corrected on this):
def profile_with_auto_build
build_profile unless profile_without_auto_build
profile_without_auto_build
end
alias_method_chain :profile, :auto_build
This also means that the association is there as soon as you need it. I guess the alternative is to hook into after_initialize but this seems to add quite a bit of overhead as it's run every time an object is initialized and there may be times where you don't care to access the association. It seems like a waste to check for its existence.
There is a gem for this:
https://github.com/jqr/has_one_autocreate
Looks like it is a bit old now. (not work with rails3)
I had an issue with this and accepts_nested_attributes_for because if nested attributes were passed in, the associated model was created there. I ended up doing
after_create :ensure_profile_exists
has_one :profile
accepts_nested_attributes_for :profile
def ensure_profile_exists
profile || create_profile
end
If you need the has_one association to exist before saving the object (when testing, for instance), you should use the after_initialize callback instead. Here is how it could be applied to your use case:
class User < ActiveRecord::Base
has_one :profile
after_initialize :build_profile, unless: :profile
end