I am currently using nested model mass assignment on one of my models. It works a treat, however, I'd like to be able to ensure that any nested models that are created "belong_to" the same user.
I've managed to implement this by using alias method chaining with:
def contact_attributes_with_user_id=(attributes)
self.contact_attributes_without_user_id = attributes.merge( "user_id" => user_id )
end
alias_method_chain :contact_attributes=, :user_id
Now this works fine, but it means I can no longer have attribute protection on user_id for the contact - which could easily catch someone out in the future.
Can anyone come up with a better way?
What if you add a before_save hook to your Contact model, like this:
belongs_to :parent
validates_presence_of :parent_id
before_save :assign_user_id
private
def assign_user_id
self.user_id = parent.user_id
end
This way your Contacts' user_ids will follow the parent model's and you don't have to worry about assigning at all (you can get rid of the alias_method_chain).
Related
I am developing a Rails 5 application in which I encountered the following difficulty.
I've got two models, let's say Kid and Toy, which are in one-to-one relationship like this:
class Kid < ActiveRecord::Base
has_one :toy
end
class Toy
belongs_to :kid, optional: true
end
So the toys can belong to zero or one kid, and from day to day it can change - it is always another kid's responsibility to look after a certain toy. Now, when I edit a toy, changing its kid is easy as can be: I just send kid_id in the strong params to update the record:
params.require(:toy).permit(:name, :type, :kid_id)
But recently, I was asked to implement the changing feature from the other way too, that is, when editing a kid, I should do something like this:
params.require(:kid).permit(:name, :age, :toy_id)
The problem is that - while belongs_to works with association_id and even has_many provides association_ids getter and setter - has_one relationship has nothing like this. What is more, has_one association gets saved the moment I call association = #record. So I simply cannot set it by sending the toy_id in the strong parameters.
I could do something like #kid.update(kid_params); #kid.toy = #toy on the controller level, but that would rather bring model logics to my controller, not to mention that I want to check if the newly assigned toy did not belong to another kid, which I imagine as some kind of validation.
The best I could come up with was to define some rails-like methods for Kid class like
def toy_id
#toy_id = toy.id unless defined?(#toy_id)
#toy_id
end
def toy_id_changed?
toy_id != toy.id
end
and set a validation and a before_commit callback
validate if: -> { toy_id.present? && toy_id_changed? } do
errors.add :toy_id, :other_has_it if new_toy.kid_id.present? && new_toy.kid_id != id
end
before_commit if: -> { toy_id_changed? } do
toy = new_toy
end
private
def new_toy
#new_toy ||= Toy.find(toy_id)
end
So far it works as expected, and now I can send toy_id in the strong params list to update a kid, and it updates the toy association if -
and only if - there is no validation error. I have even put it in a concern to be nice and separated.
My question is: isn't there a rails way to do this? haven't I reinvented the wheel?
Thanks in advance!
I want to preview what the model will look like when saved without currently saving to the database.
I am using #event.attributes = because that assigns but does not save attributes for #event to the database.
However, when I also try to assign the audiences association, Rails inserts new records into the audiences_events join table. Not cool. Is there a way to preview what these new associations will look like without inserting into the join table?
Model
class Event < ActiveRecord::Base
has_and_belongs_to_many :audiences # And vice versa for the Audience model.
end
Controller
class EventsController < ApplicationController
def preview
#event = Event.find(params[:id])
#event.attributes = event_params
end
private
def event_params
params[:event].permit(:name, :start_time, :audiences => [:id, :name]
end
end
Possible Solutions?
Possible solutions that I thought of, but don't know how to do:
Using some sort of method that assigns associations, but does not persist them.
disabling all database writes for this one action (I dont know how to do that).
Rolling back all database changes at the end of this action
Any help with these would be great!
UPDATE:
After the reading the great answers below, I ended up writing this service class that assigns the non-nested attributes to the Event model, then calls collection.build on each of the nested params. I made a little gist. Happy to receive comments/suggestions.
https://gist.github.com/jameskerr/69cedb2f30c95342f64a
In these docs you have:
When are Objects Saved?
When you assign an object to a has_and_belongs_to_many association, that object is automatically saved (in order to update the join table). If you assign multiple objects in one statement, then they are all saved.
If you want to assign an object to a has_and_belongs_to_many association without saving the object, use the collection.build method.
Here is a good answer for Rails 3 that goes over some of the same issues
Rails 3 has_and_belongs_to_many association: how to assign related objects without saving them to the database
Transactions
Creating transactions is pretty straight forward:
Event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Or
#event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Notice the use of the "bang" methods create! and destroy!, unlike create which returns false create! will raise an exception if it fails and cause the transaction to rollback.
You can also manually trigger a rollback anywhere in the a transaction by raising ActiveRecord::Rollback.
Build
build instantiates a new related object without saving.
event = Event.new(name: 'Party').audiences.build(name: 'Party People')
event.save # saves both event and audiences
I know that this is a pretty old question, but I found a solution that works perfectly for me and hope it could save time to someone else:
class A
has_many :bs, class_name 'B'
end
class B
belongs_to :a, class_name: 'A'
end
a.bs.target.clear
new_bs.each {|new_b| a.bs.build new_b.attributes.except('created_at', 'updated_at', 'id') }
you will avoid autosave that Rails does when you do a.bs = new_bs
I have 2 models - User and Alias, where User has_many :aliases.
When creating a User, it must be unique to any Alias that already exists.
When a User is created, they also get an Alias saved with that User's name.
Here is the code for my User.rb model
validates_associated :aliases # bring in any validations from relationship models
before_create :create_alias
def create_alias
a = self.aliases.new
a.alias = username
return a.save
end
The alias model validation is validates_uniqueness_of :alias.
My theory is, that before I create a User model go create an Alias model and if that fails, then creating the User model should also fail.
However, when it fails, rails is exploding.
It's not doing the validates_associated properly.
How can I accomplish what I want to do?
check this http://guides.rubyonrails.org/association_basics.html#has-many-association-reference, you can use the :autosave and :validate options when you define the association so you can just remove that validates_associated and your before_create callback and let rails handle those actions
has_many :aliases
EDIT: I think the problem is that you are assigning new alias AFTER the validations! so, you have to build the new alias right when you assign the username of after validations
def username=(value) #custom setter for username
aliases.build(alias: value)
write_attribute(:username, value)
end
now the alias will be there before the validations and user.valid? will run the alias' valdations too when validating
This must be impossible to do in a model. At least with devise...
To fix this problem I just gave up trying and moved the config to the controller with something like this:
#user.aliases.build(:name => #user.username)
In Rails, how can you update a child record with values from its parent before either the child or parent are saved?
I'm using Rails' nested attributes to create a parent record with many children. The parent record has a user_id attribute to track its owner, and for purposes outside of the scope of this question, the child record also needs to store this user_id attribute.
Currently, I'm using a before_save callback on the child model:
before_save :set_user_id
def set_user_id
if user = self.parent.try(:user)
self[:user_id] = user.id
end
end
But because I also need to make sure that the child is unique for a given parent/user combo, I have to do this:
validates :parent,
:uniqueness => {:scope => :user_id},
:unless => Proc.new{|child| child.user_id.blank?}
The problem is that unless parameter. The child's user_id attr is blank until after validation, so the validation effectively isn't run on child creation. This causes a problem if users double-submit the form; the result is that duplicate, invalid subscriptions end up in the database. I've tried to fix this by changing this:
before_save :set_user_id
to
before_validate :set_user_id
but it appears the parent's user_id is not accessible before validation.
Has anybody encountered such a problem?
If possible, can you just put a hidden field in the form with the user_id in it? You'll probably need to do some validation and potential rollback in an after_save, but seems like it might be easy.
Another option might be to just set the user_id for all the relevant objects in the params hash in the controller before calling the save/create on the parent object.
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