Rails has_many_polymorphs in reverse? - ruby-on-rails

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.

Related

upgrade to Rails 5.2: how to avoid "optional: true" for non-optional associations?

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.

How to save related Models in one transaction?

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

Add association (<<) without committing to database

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.

nested record can't validate because its nested belongs_to doesn't save

I'm trying to create a nested child and grandchild record. The child belongs_to both the parent and the grandchild. The child won't validates_presence_of the grandchild because it hasn't been saved yet.
I'm using Rails 2.3.11, Formtastic, InheritedResources, and Haml, and everything else seems to work correctly - for example, validation errors on the grandchild populate properly in the parent form, and the invalid values are remembered and presented to the user. The parent model doesn't even try to update unless everything is valid, just as it should be.
My code is something like this, though in a different problem domain:
class Project < ActiveRecord::Base
has_many :meetings, :dependent => :destroy
accepts_nested_attributes_for :meetings
end
class Meeting < ActiveRecord::Base
belongs_to :project
belongs_to :task
accepts_nested_attributes_for :task
validates_presence_of :task_id, :project_id
end
class Task < ActiveRecord::Base
has_many :meetings, :dependent => :destroy
end
The Project ALWAYS exists already, and may already have Meetings that we don't want to see. Tasks may belong to other Projects through other Meetings, but in this case, the Task and Meeting are ALWAYS new.
In the controller, I build a blank record only on the new action
#project.meetings.build
and save the data like this:
#project.update_attributes(params[:project])
and in the view
- semantic_form_for #project do |f|
- f.semantic_fields_for :meetings do |m|
- next unless m.object.new_record?
= m.semantic_errors :task_id
- m.object.build_task unless i.object.task
- m.semantic_fields_for :task do |t|
- f.inputs do
= t.input :task_field
= m.input :meeting_field
When I try to save the form, I get a validation error of "Task can't be blank." Well, sure, the Task hasn't been saved yet, I'm trying to validate, and I don't have an ID for it.
Is there a simple and elegant way to make sure that the grandchild record (Task) gets built before the child record?
I've tried something like this in the Meeting model:
before_validation_on_create do |meeting|
meeting.task.save if meeting.task.valid?
end
and that seems to save the Task, but the Meeting still doesn't get the right ID. Same error, but the Task record gets created.
I've also tried this:
before_validation_on_create do |meeting|
new_task = meeting.task.save if meeting.task.valid?
meeting.task = new_task
end
Which has the strange behaviour of raising ActiveRecord::RecordNotFound "Couldn't find Task with ID=XX for Meeting with ID=" - which I sort of get, but seems like a red herring.
I also tried adding :inverse_of to all the relationships and validating :task instead of :task_id. The latter, oddly, fails but seems to give no error message.
My actual goal here is to create more than one Task, each with an initial Meeting on a previously selected Project... so I could take another approach with my problem - I could do something simple and ugly in the controller, or create the first Meeting in an after_create on the Project. But this is so pretty and soooo close to working. The fact that I'm getting proper validation errors on :task_field and :meeting_field implies that I'm on the right track.
I see what the problem is, but not how to solve it: I suspect I'm missing something obvious.
Thank you!
Well, I found a solution, based on one of the similar questions out there, but the short of it is "rails 2.3 doesn't seem to be very good at this." I think I can put the answer in a more succinct way than any of the other answers I've seen.
What you do is you skip the validation of the :task_id, but only if task is valid! Most of the other answers I've seen use a proc, but I think it's more readable using delegate, like this:
delegate :valid?, :to => :task, :prefix => true, :allow_nil => true
validates_presence_of :task_id, :unless => :task_valid?
I also had another problem hidden under the waterline - in the case, the "Project" is actually a special sort of record that I wanted to protect, which has a validation that (intentionally) fails only for this special record, and I also set readonly? to true for the special record.
Even though I'm not actually changing that special record, it still needs to validate and can't be readonly to update children through it. For some reason, I wasn't seeing the error message for that validation. To solve that, I made the validation on the Project only applicable :on => :create, and I took out the readonly? thing.
But the general solution is "don't validate presence of the unbuilt belongs_to object if the object itself is valid." Nil is never valid, therefore the validation still works if you just have an object_id.
(Please don't vote down a sincere question unless you have an answer or a link to one. I'm aware the question has been asked by others in other ways, I read many of those other questions, none seemed to be precisely the same problem, and I had not found a solution.)

Including an association if it exists in a rails query

Update: This may be something that just isn't doable. See this
TLDR: How do you conditionally load an association (say, only load the association for the current user) while also including records that don't have that association at all?
Rails 3.1, here's roughly the model I'm working with.
class User
has_many :subscriptions
has_many :collections, :through => :subscriptions
end
class Collection
has_many :things
end
class Thing
has_many :user_thing_states, :dependent => :destroy
belongs_to :collection
end
class Subscription
belongs_to :user
belongs_to :collection
end
class UserThingState
belongs_to :user
belongs_to :thing
end
There exist many collections which have many things. Users subscribe to many collections and thereby they subscribe to many things. Users have a state with respect to things, but not necessarily, and are still subscribed to things even if they don't happen to have a state for them. When a user subscribes to a collection and its associated things, a state is not generated for every single thing (which could be in the hundreds). Instead, states are generated when a user first interacts with a given thing. Now, the problem: I want to select all of the user's subscribed things while loading the user's state for each thing where the state exists.
Conceptually this isn't that hard. For reference, the SQL that would get me the data needed for this is:
SELECT things.*, user_thing_states.* FROM things
# Next line gets me all things subscribed to
INNER JOIN subscriptions as subs ON things.collection_id = subs.collection_id AND subs.user_id = :user_id
# Next line pulls in the state data for the user
LEFT JOIN user_thing_states as uts ON things.id = uts.thing_id AND uqs.user_id = :user_id
I just don't know how to piece it together in rails. What happens in the Thing class? Thing.includes(:user_thing_states) would load all states for all users and that looks like the only tool. I need something like this but am not sure how (or if it's possible):
class Thing
has_many :user_thing_states
delegates :some_state_property, :to => :state, :allow_nil => true
def state
# There should be only one user_thing_state if the include is correct, state method to access it.
self.user_thing_states.first
end
end
I need something like:
Thing.includes(:user_question_states, **where 'user_question_state.user_id => :user_id**).by_collections(user.collections)
Then I can do
things = User.things_subscribed_to
things.first.some_state_property # the property of the state loaded for the current user.
You don't need to do anything.
class User
has_many :user_thing_states
has_many :things, :through => :user_thing_states
end
# All Users w/ Things eager loaded through States association
User.all.includes(:things)
# Lookup specific user, Load all States w/ Things (if they exist for that user)
user = User.find_by_login 'bob'
user.user_thing_states.all(:include => :things)
Using includes() for this already loads up the associated object if they exist.
There's no need to do any filtering or add extra behavior for the Users who don't have an associated object.
Just ran into this issue ourselves, and my coworker pointed out that Rails 6 seems to include support for this now: https://github.com/rails/rails/pull/32655
*Nope, didn't solve it :( Here's a treatment of the specific issue I seem to have hit.
Think I've got it, easier than expected:
class Thing
has_many :user_thing_states
delegates :some_state_property, :to => :state, :allow_nil => true
scope :with_user_state, lambda { |user|
includes(:user_thing_states).where('user_thing_states.user_id = :user_id
OR user_thing_states.user_id IS NULL',
{:user_id => user.id}) }
def state
self.user_thing_states.first
end
end
So:
Thing.with_user_state(current_user).all
Will load all Things and each thing will have only one user_question_state accessible via state, and won't exclude Things with no state.
Answering my own question twice... bit awkward but anyway.
Rails doesn't seem to let you specify additional conditions for an includes() statement. If it did, my previous answer would work - you could put an additional condition on the includes() statement that would let the where conditions work correctly. To solve this we'd need to get includes() to use something like the following SQL (Getting the 'AND' condition is the problem):
LEFT JOIN user_thing_states as uts ON things.id = uts.thing_id AND uqs.user_id = :user_id
I'm resorting to this for now which is a bit awful.
class User
...
def subscribed_things
self.subscribed_things_with_state + self.subscribed_things_with_no_state
end
def subscribed_things_with_state
self.things.includes(:user_thing_states).by_subscribed_collections(self).all
end
def subscribed_things_with_no_state
Thing.with_no_state().by_subscribed_collections(self).all
end
end

Resources