Is it possible in Rails to add an association to an existing record without immediately committing this change to the database?
E.g. if I have Post has_many :tags
post.tags << Tag.first
This will commit to database immediately. I've tried other ways instead of <<, but without success (what I want is to create the association when saving the parent object).
Is it possible to get behavior like when you are adding association to a new record with build?
post.tags.build name: "whatever"
I think this is kind of inconsistent in Rails, in some cases it would be useful to have an option to do this.
In other words I want
post.tags << Tag.first # don't hit the DB here!
post.save # hit the DB here!
This should work in Rails 3.2 and Rails 4:
post.association(:tags).add_to_target(Tag.first)
See this gist: https://gist.github.com/betesh/dd97a331f67736d8b83a
Note that saving the parent saves the child and that child.parent_id is NOT set until you save it.
EDIT 12/6/2015:
For a polymorphic record:
post.association(:tags).send(:build_through_record, Tag.first)
# Tested in Rails 4.2.5
post_tag = post.post_tags.find_or_initialize_by_tag_id(Tag.first.id)
post_tag.save
To add to Isaac's answer, post.association(:tags).add_to_target(Tag.first) works for has_many relationships, but you can use post.association(:tag).replace(Tag.first, false) for has_one relationships. The second argument (false) tells it not to save; it will commit it to the database by default if you leave the argument empty.
PREFACE This is not exactly an answer to this question but somebody searching for this kind of functionality may find this useful. Consider this and other options very carefully before wantonly putting it into a production environment.
You can, in some cases, leverage a has_one relationship to get what you want. Again, really consider what you're trying to accomplish before you use this.
Code to Consider
You have a has_many relationship from Trunk to Branch and you want to add a new branch.
class Trunk
has_many :branches
end
class Branch
belongs_to :trunk
end
I can also relate them to each other singularly. We'll add a has_one relationship to the Trunk
class Trunk
has_many :branches
has_one :branch
end
At this point, you can do things like Tree.new.branch = Branch.new and you'll be setting up a relationship that will not save immediately, but, after saving, will be available from Tree.first.branches.
However, this makes for quite a confusing situation for new developers when they look at the code and think, "Well, which the hell is it supposed to be, one or many?"
To address this, we can make a more reasonable has_one relationship with a scope.
class Trunk
has_many :branches
# Using timestamps
has_one :newest_branch, -> { newest }, class_name: 'Branch'
# Alternative, using ID. Side note, avoid the railsy word "last"
has_one :aftmost_branch, -> { aftmost }, class_name: 'Branch'
end
class Branch
belongs_to :trunk
scope :newest, -> { order created_at: :desc }
scope :aftmost, -> { order id: :desc }
end
Be careful with this, but it can accomplish the functionality being asked for in the OP.
Related
After moving my application from Rails 4.2.8 to 5.2.3 the inserts fail with
Billings event must exist
The application receives a single cascaded hash with one event and many associated billings and should put this into the database in one single transaction; this did always work before.
class Event < ActiveRecord::Base
has_many :billings, -> { where('1 = 1') }, dependent: :destroy
accepts_nested_attributes_for :billings
validates_associated :billings
end
class Billing < ActiveRecord::Base
belongs_to :event
validates_presence_of :event_id, on: :update
end
class EventsController < ApplicationController
def kC
#event = Event.new(event_params)
if #event.save
[doesn't get here anymore]
end
end
end
There is no controller for billings, they do only exist via their associated event.
Quick analyses finds in the docs mention that
belongs_to :event, optional: true
would avoid this error, and it indeed does. But this seems very wrong to me, because in this application billings must never exist without their event, it is NOT optional!
But then, what is the correct solution?
Further analysis show: all validations get processed, but a before_create() callback is never reached. The "must exist" error is added at some internal place, it does not come from my code.
Furthermore, when creating a template with only the code as shown above, I found the problematic code to be the scoping -> { where('1 = 1') }
In the real application this is a more complex (and more useful) term, but this simple and seemingly transparent term triggers the problem just the same.
There are many similar questions here, but then, many have a situation where the association is indeed optional, some have nonstandard namings (I don't think I have, as it worked before), and I didn't find one for this case where the belonging model is fully handled via the having one.
In Rails 5, whenever we define a belongs_to association, it is required to have the associated record present by default. It triggers validation error if the associated record is not present. To remove this default behavior, we can use new_framework_defaults.rb initializer which comes with Rails 5.
(For more info you can check this https://github.com/rails/rails/pull/18937)
When upgrading from the older version of Rails to Rails 5, we can add this initializer by running bin/rails app:update task.
This newly added initializer has the following config flag that handles the default behavior
Rails.application.config.active_record.belongs_to_required_by_default = true
We can turn off this behavior by setting its value to false
Rails.application.config.active_record.belongs_to_required_by_default = false
I found what appears to be the correct solution:
class Event < ActiveRecord::Base
has_many :billings, -> { where('1 = 1') }, dependent: :destroy, inverse_of: :event
accepts_nested_attributes_for :billings
validates_associated :billings
end
Adding this inverse_of: option in this way resolves the problem.
Preliminary tentative root cause analysis:
The (sparse) documentation for the inverse_of option does suggest to add it to the belongs_to feature; it does not mention adding it to has_many (it does not discourage that either). Adding it to belongs_to does not improve things in this case, also the use-case in the documentation does not apply here.
Nevertheless, the documentation mentions an "automatic guessing" of associations, and that this automatic guessing would be omitted in certain cases as declared in AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS.
Searching for this term in the source leads to a private method can_find_inverse_of_automatically?(), where it becomes obvious that also a scope will result in the automatic guessing being omitted.
It appears that the unravelling of a cumulative insert in some way needs to pinpoint the "inverse_of" (be it automatically or coded), or otherwise it would consider the owning relation as nonexisting - with the latter, due to the mentioned change in Rails 5, now leading to a validation error.
I have two models:
class Customer < ActiveRecord::Base
has_many :contacts
end
class Contact < ActiveRecord::Base
belongs_to :customer
validates :customer, presence: true
end
Then, in my controller, I would expect to be able to create both in
"one" sweep:
#customer = Customer.new
#customer.contacts.build
#customer.save
This, fails (unfortunately translations are on, It translates to
something like: Contact: customer cannot be blank.)
#customer.errors.messages #=> :contacts=>["translation missing: en.activerecord.errors.models.customer.attributes.contacts.invalid"]}
When inspecting the models, indeed, #customer.contacts.first.customer
is nil. Which, somehow, makes sense, since the #customer has not
been saved, and thus has no id.
How can I build such associated models, then save/create them, so that:
No models are persisted if one is invalid,
the errors can be read out in one list, rather then combining the
error-messages from all the models,
and keep my code concise?
From rails api doc
If you are going to modify the association (rather than just read from it), then it is a good idea to set the :inverse_of option on the source association on the join model. This allows associated records to be built which will automatically create the appropriate join model records when they are saved. (See the ‘Association Join Models’ section above.)
So simply add :inverse_of to relationship declaration (has_many, belongs_to etc) will make active_record save models in the right order.
The first thing that came to my mind - just get rid of that validation.
Second thing that came to mind - save the customer first and them build the contact.
Third thing: use :inverse_of when you declare the relationship. Might help as well.
You can save newly created related models in a single database transaction but not with a single call to save method. Some ORMs (e.g. LINQToSQL and Entity Framework) can do it but ActiveRecord can't. Just use ActiveRecord::Base.transaction method to make sure that either both models are saved or none of them. More about ActiveRecord and transactions here http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
I need to associate two models with a simple has_many. The problem is that I don't want to use the id (_id) as the primary key for the association. I still want the model to keep using the default ObjectIds for everything else.
(This is running on Rails3.1 + Mongoid)
So basically I want:
class Message
...
field :message_id, :default => proc { "fail-#{Time.now.to_f.to_s}" }
...
has_many :message_reports, primary_key: :message_id, foreign_key: :message_id
...
end
class MessageReport
...
field :message_id, :default => proc { "fail-#{Time.now.to_f.to_s}" }
...
has_many :message, primary_key: :message_id, foreign_key: :message_id
...
end
This would only work for ActiveRecord. Mongoid don't support the primary_key option.
So how do I get the same results for Mongoid collections?
Before you say: don't do that...
The reason I really really need to kay on this field and not the proper id is that these are messages... and the message_ids are unique ids returned by the API I call to send a message. Later the same id is received in callbacks from the other side.
I could just do queries and stick it in a method to find the "associated" reports from a message and vice versa... I'd rather have them be actual associations, if possible.
I could force the report-recieving process to search for and match up the objects for the association... but I'd rather not put that responsibility there when it is kind-of superfluous and it has nothing more to do with this data besides validating and saving it.
In short: I'd prefer an association :)
This feature doesn't exist on Mongoid actually even on Master and it's not planned in Mongoid 3.0
Do some feature request. The Mongoid community is really open to add some new feature if it's a good idea. To me It's a good idea.
I have three models, basically:
class Vendor
has_many :items
end
class Item
has_many :sale_items
belongs_to :vendor
end
class SaleItem
belongs_to :item
end
Essentially, each sale_item points to a specific item (but has an associated quantity and sale price which might be different from the item's base price, hence the separate model), and each item is made by a specific vendor.
I'd like to sort all sale_items by vendor name, but this means going through the associated item, because that's where the association is.
My first attempt was to change SaleItem to the following:
class SaleItem
belongs_to :item
has_one :vendor, :through => :item
end
Which allows me to look for SaleItem.first.vendor, but doesn't allow me to do something like:
SaleItem.joins(:vendor).all(:order => "vendors.name")
Is there an easy way to figure out these complex associations and sorting? It would be especially great if there were a plugin that could take care of these sort of things. I have a lot of different types of tables to add sorting to in this application, and I feel like this will be a big chunk of the figuring-out work.
This could definitely be done with a more complex SQL query (possibly using find_by_sql), but you could also do it pretty easily in Ruby. Try something like the following:
SaleItem.find(:all, :include => { :items => :vendors }).sort do |first,second|
first.vendor.name <=> second.vendor.name
end
I haven't tested it, so it might not work exactly like this, but it should give you a good idea of one possible solution.
Edit: Found an old blog post that seems to have solved this issue. Hopefully this still works in the lastest version of ActiveRecord.
source: http://matthewman.net/2007/01/04/eager-loading-objects-in-a-rails-has_many-through-association/
Second Edit: Straight from the Rails documentation
To include a deep hierarchy of associations, use a hash:
for post in Post.find(:all, :include => [ :author, { :comments => { :author => :gravatar } } ])
That’ll grab not only all the comments but all their authors and gravatar pictures. You can mix and match symbols, arrays and hashes in any combination to describe the associations you want to load.
There's your explanation.
Do you really need your sale_items sorted by the database, or could you wait until it is presented and do the sorting client side via javascript (there are some great sorting libraries out there) - that would save server CPU and (backend) code complexity.
So I have some models set up that can each have a comment. I have it set up using has_many_polymorphs, but I'm starting to run into some issues where it's not working how I think it should.
For example:
class Project < ActiveRecord::Base
end
class Message < ActiveRecord::Base
has_many_polymorphs :consumers,
:from => [:projects, :messages],
:through => :message_consumers,
:as => :comment # Self-referential associations have to rename the non-polymorphic key
end
class MessageConsumer < ActiveRecord::Base
# Self-referential associations have to rename the non-polymorphic key
belongs_to :comment, :foreign_key => 'comment_id', :class_name => 'Message'
belongs_to :consumer, :polymorphic => true
end
In this case, the Message wouldn't get deleted when the Project is removed, because the Message is really the parent in the relationship.
I simplified it a little for the example, but there are other models that have have a Message, and there are also Attachments that work similarly.
What would be the correct way to set this up so that the children get removed when the parent is deleted? I'm hoping to not have a million tables, but I can't quite figure out another way to do this.
When you say "so that the children get removed when the parent is deleted?", can you give an example? I.e. when a project is deleted I want all its messages to be deleted too? What happens when you delete a message, do you want anything else (e.g. all corresponding message_consumer entries) to be deleted as well?
UPDATE
OK, so has_many_polymorphs will automatically delete "orphaned" message_consumers. Your problem is that a message may have more than one consumer, so deleting a project may not be sufficient grounds for deleting all its associated messages (as other consumers may depend on those messages.)
In this particular case you can set up an after_destroy callback in MessageConsumer, to check whether there still exist other MessageConsumer mappings (other than self) that reference the Message and, if none exist, also delete the message, e.g.:
class MessageConsumer < ActiveRecord::Base
...
after_destroy :delete_orphaned_messages
def delete_orphaned_messages
if MessageConsumer.find(:first, :conditions => [ 'comment_id = ?', self.comment_id] ).empty?
self.comment.delete
end
end
end
All this is happening inside a transaction, so either all deletes succeed or none succeed.
You should only be aware of potential race conditions whereby one session would arrive at the conclusion that a Message is no longer used, whereas another one may be in the process of creating a new MessageConsumer for that exact same Message. This can be enforced by referential integrity at the DB level (add foreign key constraints, which will make on of the two sessions fail, and will keep your database in a consistent state), or locking (ugh!)
You could simplify this a lot by using acts_as_commentable.