Rails - Best-Practice: How to create dependent has_one relations
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
How to create dependent has-one relation with arguments
Typically this will create from accepts_nested_attribute_for, as you've demonstrated, assuming this is passed from your form. You could try a customised validaiton
validate :check_has_b
def check_has_b
unless b and b.valid?
errors.add(:base, "You must have a valid B ")
end
end
Rails: mostly has_one / sometimes has_many relationship
I think you should cover the general case, when Cover has_many :colors
. So this is a many-to-many relationship. In this case, you will have 2 options:
Use join table, if you just need to store
cover_id
andcolor_id
in the join table, so you will usehas_and_belongs_to_many
to define your association. With this option, You will have to create join table.Use join model, if you need to store not only
cover_id
andcolor_id
, but also other attributes, and you need to do some calculation, so you have to usehas_many :through
to define your association. With this option, You will have to create new model.
Automatically insert new record into association when using has_one or has_many
I'm doing something similar with user & their settings, probably this could work for you:
class User < ActiveRecord::Base
#...
has_one :role
before_create :build_a_role
private
def build_a_role
self.build_role(role_att: value)
end
end
Then, when the user is saved, the role is saved too.
Ordering through has_one association
Try this:
Person.joins(:person_detail).order('person_details.detail_field')
Retrieve default belongs_to record from a built has_many association in a before_create hook?
I agree with you that what you describe should work, but if you're building associated records in memory and expecting them to save together properly -- with everything hooked up -- then ActiveRecord is particularly finicky about how you define them.
But you can make it work without changing a thing in your before_create
.
The usual problem is that you need to give AR hints about which relationships are inverses of each other. The has_many
and belongs_to
methods take an :inverse_of
option. The problem in your case is that you have one side of a relationship (Stash#owner
) that is actually the inverse of two on the other (User#stashes
and User#default_stash
), so what would you set as the inverse_of
for Stash#owner
?
The solution is to add a has_one
to Stash (call it something like owner_as_default
), to balance things out. Then you can add inverse_of
to all of the definitions, each identifying its inverse on the other side. The end result is this:
class User < ActiveRecord::Base
has_many :stashes, foreign_key: :owner_id, inverse_of: :owner
belongs_to :default_stash, class_name: "Stash", inverse_of: :owner_as_default
...
end
class Stash < ActiveRecord::Base
belongs_to :owner, class_name: "User", inverse_of: :stashes
has_one :owner_as_default, class_name: "User",
foreign_key: :default_stash_id, inverse_of: :default_stash
end
(Also, you don't need a foreign key on your belongs_to.)
From there, your before_create
should work as you've written it.
Yes, it seems that this is a lot of redundant defining. Can't ActiveRecord figure it all out from the foreign keys and whatnot? I always feel like I'm breaking some walls by making one side so aware of what it's the inverse of. Maybe in some future version of Rails this will be ironed out.
Related Topics
Putting French (Accented) Characters in Ruby File
Rufus Scheduler Not Running in Production
Libmysqlclient.So.15: Cannot Open Shared Object File: No Such File or Directory
How to Update Ruby on Linux (Ubuntu)
Rails Sends 0 Byte Files Using Send_File
Mavericks, Rbenv, Your Ruby Version Is 2.0.0, But Your Gemfile Specified 2.1.1
Recommended Development Web Server for Ruby on Rails 3
Setting Up Rake-Pipeline for Use with Handlebars Alongside Google App Engine
Rails - Render :Action to Target Anchor Tag
How to Avoid Trailing Empty Items Being Removed When Splitting Strings
Google Plus API Shutdown Today, Which Alternative Can Be Used to Authentication
Rails Catch-All/Globbing Routes
Error: Error Installing Ffi: Error: Failed to Build Gem Native Extension
Convert Utc to Local Time in Rails 3
How to Test a Function With Gets.Chomp in It
How to Get Filename Without Extension from File Path in Ruby