Rails 5. Update association with exception handling - ruby-on-rails

I have a one-to-many relationship between the classes P(parent) and C(childs).
Table C has a unique composite index {p_id, somerow}.
Having 2 objects of class P (p1 and p2), I want to combine them into one, doing this through
p2.childs.update_all (parent: p1), but I get a rollback of the transaction, because the uniqueness of the composite index is violated. However, from the point of view of internal logic, this situation is not an error, and a duplicate entry, instead of changing the parent, must be destroyed.
What is the most correct way to solve the problem?
P.S. The number of requests to the database is critical.
P.P.S. The number of children in the relation can exceed the value in 1k records.

If you are ready to validate records by Rails, which means execute a separate query to validate each record, here is a straightforward way to do it:
class C < ActiveRecord::Base
validates_uniqueness_of :somerow, scope: :p_id
belongs_to :p
end
class P < ActiveRecord::Base
has_many :childs
def merge_sibling(p2)
p2.childs.each do |c|
c.p_id = self.id
c.valid? ? c.save : c.destroy
end
p2.destroy
end
end
p1.merge_sibling p2

Related

How to get active record relation of polymorphic parents from child object?

I have below structure:
class Transaction < ApplicationRecord
belongs_to :transactionable, polymorphic: true
end
class TransactionSale < ApplicationRecord
has_many :transactions, as: :transactionable
end
class TransactionRental < ApplicationRecord
has_many :transactions, as: :transactionable
end
I want to get all the linked transactions (sale & rental) by querying the Transaction table only.
For example:
Transaction.includes(:transactionable).where(project_id: project_id).map { |txn| txn.transactionable }
The above query returns both TransactionSale and TransactionRental objects combined and that is the exact result I wanna achieve. The only problem here is it returns a ruby array instead of ActiveRecord::Relation so I can't sort the transactions further in a single query or use other active record methods.
I have read all other answers that suggest plucking the ids and then apply where which is not possible here coz I don't know whether the table is rental or sale.
Is there any way to achieve the same result without losing the ActiveRecord relation?
No nice/easy way, no. You could do it by creating the SQL and running a UNION as a subquery but that's pretty ugly.
Apparantly i solved it myself,
# Get id and classes of all transaction types
locks = Transaction.where(project_id: self.id).pluck(:transactionable_id, :transactionable_type)
# Init empty active record relation
final = Transaction.none
# convert id and classes array to hash to group on polymorphic classes
locks.group_by { |s| s[1] }.each_pair do |k,v|
# extracts ids to make query
ids = v.map { |d| d[0] }
# make query from respective model
data = eval(k).where(id: ids)
# merge the array returned above with empty relation and rewrite
final = final.or(data)
end
The final contains the active record relation of all polymorphic records.

ActiveRecord seems to be validating unchanged child records unnecessarily

I have found a situation where ActiveRecord is validating child records seemingly unnecessarily. Apologies in advance for the length as this is quite complex.
This involves through associations that have previously been used but not changed in any way. It occurs on 3.2 through to recent master. I am not sure if it is a design decision that has led to unexpected behaviour, or a bug of some kind.
I have reduced a test case from the actual code as follows:
Models:
class A < ActiveRecord::Base
belongs_to :b
has_many :cs, :through => :b
before_validation { puts "A" }
end
class B < ActiveRecord::Base
has_many :as
has_many :cs
before_validation { puts "B" }
end
class C < ActiveRecord::Base
belongs_to :b
before_validation { puts "C" }
end
Migration:
class AddABC < ActiveRecord::Migration
def change
create_table :as do |t|
t.references :b
end
create_table :bs do |t|
end
create_table :cs do |t|
t.references :b
end
end
end
The reduced test case that triggers it is this when run on an empty database:
b = B.create!
c = C.create!
b.cs << c
a = A.new
a.b = b
a.cs.first
puts "X"
a.valid?
which gives output:
B
C
C
X
A
C
Which shows that validating an A validates its Cs.
Now having looked into this I am aware of the has_many :validate => false option, and using it, the problem goes away. But it seems to me there’s more going on here than this - bear with me.
The AR docs say:
:validate If false, don't validate the associated objects when saving the parent object. true by default.
But I find this confusing as this clearly cannot mean all records. It won’t validate the objects if I never get the association (remove a.cs.first from the code above), or I get it but never use it (replace with a.cs). This is because it goes through validate_collection_association in lib/active_record/autosave_association.rb which includes the code:
def validate_collection_association(reflection)
if association = association_instance_get(reflection.name)
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
records.each_with_index { |record, index| association_valid?(reflection, record, index) }
end
end
end
It's all conditional on association_instance_get which fetches from the association cache. No cache means no records to validate.
I have tried to do a simpler has_many, by setting up just a B model which references A, but then I’ll need to create the B before the A, then A will no longer be a new record if I try to save it, and this code prevents the problem as the branch called will no longer be the first:
def associated_records_to_validate_or_save(association, new_record, autosave)
if new_record
association && association.target
elsif autosave
association.target.find_all(&:changed_for_autosave?)
else
association.target.find_all(&:new_record?)
end
end
The only real explanation I can come up with for it only validating loaded records is because the intention of ActiveRecord here is to validate changed records only. Really I’d expect it to validate if and only if it’s going to save, and hence the default autosave option of only saving changed records should prevent a validation.
I have found a related ticket and the commit 27aa4dda7d89ce733 (not yet in any release I think) which makes a change but does not fix this specific issue from my testing. It does however contain the expression:
!record.persisted? || record.changed? || record.marked_for_destruction?
and if I add this condition to the innermost loop of validate_collection_association then the problem goes away, with the ActiveRecord tests still passing on my machine.
This has been a significant performance issue in my project because the model in question was only supposed to be validated in admin, where an unindexed field used in a custom validation was acceptable due to the rarity of it being saved and thus I judged that indexing it would be over-indexing (it wouldn’t just be one field). Obviously in most cases this over-validation would be far less serious, and it only seems to happen in quite a specific case, so this could be a bug.
So, while I’ve got a good idea as to what is happening, I’m not entirely sure what should be happening, which is why I haven’t filed this as an ActiveRecord ticket. Do you think this is a bug? Why is it working like this? What is the validate option really for? If this is a bug, can you explain why the code works this way, and why it is overreaching? What case would my code change to ActiveRecord above break?
The reason this is happening is because the relationship between A and C is through B.
Before you assign a.b = b, a has no bs or cs.
If you assign a.b = b but you don't call a.cs, then a has no reason to try to load the associated cs. has_many only creates the cs convenience method, it does not call it for you. Here only a.b_id is set to b.id.
Once you call a.cs, a will look for associated cs objects through b since b is available. It will find those objects and add them as children to a.
I see your point, that technically, there is nothing to do in this particular case in this particular schema for the cs, but I can see why ActiveRecord is checking. These objects, as far as it is concerned, are children of a and children records are validated unless specifically told not to through validate: false.
In this case, a is a child of b, so a is not required to validate it.
In general, parents will cause their associated children to be validated. Children do not have to validate their parents.

After_save in Rails

I have two models Person and Address and a join table Persons::Address containing (person_id, address_id). If my Person object is p, then I do p.address_ids to get an array of address ids of that person.
Now, I have an after_save callback which uses p.address_ids. Let's say I had p.address_ids = [a,b,c]. I ran these two statements after this.
a.address_ids = [a,b,d]
a.save
In the after_save callback, if I do self.address_ids, I'll get [a,b,d]. I want to make an array A = [a,b,c,d] which should contain the elements of (last array + current array).uniq , what should I do?
Update :
In simple words, I want something like p.address_id_was (ActiveModel::Dirty) for associations like `p.address_ids'.
It's a many-to-many relationship, and I think you're doing it wrong.
The join table should be called addresses_people you can use this migration to generate it:
class CreateJoinTableAddressPerson < ActiveRecord::Migration
def change
create_join_table :addresses, :people
add_index :addresses_people, [:address_id, :person_id], unique: true
end
end
I've put a unique index on the composite key to prevent the same relationship being entered more than once.
Make sure your Person class has has_and_belongs_to_many :addresses and your Address class has has_and_belongs_to_many :people
Let's suppose you have a Person object p and this person has two addresses a and b you can get the collection of p's addresses by saying p.addresses and you can get the collection of people that live at address a by saying a.people
If you want to add more addresses to p
p.addresses << c
p.addresses << d
if you add an address that is already related to p it won't be duplicated, nothing will change.
Try this:
a.address_ids = a.address_ids|[a,b,d]
what if you store old address_ids in before_save callback, and then use it with new ones to create joined array?

How to actually use has_many :through and set up relationships

So many tutorials on how to set up a has_many :through but not enough on how to actually do it!
I have a Inventories and Requests table joined by Bookings. Example: there could be 3 lenders who have tents in inventory, each of which is requested by 3 other borrowers. What I want to do is for each of the 3 tents in inventory, show that lender the list of 3 borrowers who requested the tent. Then the lender can pick who s/he wants to be the ultimate borrower.
I have thoughts on how this should work, but no idea if it's right, so please give advice on the below! The action is driven all by the Requests controller. Let's run through an example where the Inventories table already has 3 tents, ids [1, 2, 3]. Let's say Borrower Pat submits a Request_ID 1 for a tent.
Am I then supposed to create 3 new Bookings all with Request_ID 1 and then Inventory_ID [1, 2, 3] to get all the conceivable combinations? Something like
Inventory.where(name: "tent").each { |inventory| #request.bookings.create(inventory_id: inventory.id) }
And then is it right to use the Bookings primary key as the foreign key in both the Request and Inventory? Which means that after Borrower Pat submits his request, the bookings_id will be blank until say Lender 2 accepts, at which point bookings_id equals the id that matches the combination of Request_ID 1 and Inventory_ID 2
Now let's say when a Request is posted and a Bookings is made, I email the lender. However, I realized I don't want to bother Lender Taylor if 3 borrowers want her tent over the same time period. I'll just email her the first time, and then the subsequent ones she'll find out about when she logs in to say yes or no. In this situation is it OK to just query the Bookings table in the create action, something like (expanding off above)
-
Inventory.where(name: "tent").each do |inventory|
if !Bookings.find_by_inventory_id(inventory.id).exists?
# If there are no other bookings for this inventory, then create the booking and send an email
#request.bookings.create(inventory_id: inventory.id)
AlertMail.mail_to_lender(inventory).deliver
else
# If there are other bookings for this inventory, do any of those bookings have a request ID where the requested time overlaps with this new request's requested time? If so then just create a booking, don't bother with another email
if Bookings.where(inventory_id: inventory.id).select { |bookings_id| Bookings.find_by_id(bookings_id).request.time overlaps_with current_request.time }.count > 0
#request.bookings.create(inventory_id: inventory.id)
# If there are other bookings for this inventory but without overlapping times, go ahead and send an new email
else
#request.bookings.create(inventory_id: inventory.id)
AlertMail.mail_to_lender(inventory).deliver
end
end
end
Code above is probably flawed, I just want to know the theory of how this is supposed to be working.
Join Table
Firstly, has_many :through works by using a join table - a central table used to identify two different foreign_keys for your other tables. This is what provides the through functionality:
Some trivia for you:
has_and_belongs_to_many tables are called [plural_model_1]_[plural_model_2] and the models need to be in alphabetical order (entries_users)
has_many :through join tables can be called anything, but are typically called [alphabetical_model_1_singular]_[alphabetical_model_2_plural]
--
Models
The has_many :through models are generally constructed as such:
#app/models/inventory.rb
Class Inventory < ActiveRecord::Base
has_many :bookings
has_many :requests, through: :bookings
end
#app/models/booking.rb
Class Booking < ActiveRecord::Base
belongs_to :inventory
belongs_to :request
end
#app/models/request.rb
Class Request < ActiveRecord::Base
has_many :bookings
has_many :requests, through: :bookings
end
--
Code
Your code is really quite bloated - you'll be much better doing something like this:
#app/controllers/inventories_controller.rb
Class InventoriesController < ApplicationController
def action
#tents = Inventory.where name: "tent"
#tents.each do |tent|
booking = Booking.find_or_create_by inventory_id: tend.id
AlertMail.mail_to_lender(tent).deliver if booking.is_past_due?
end
end
end
#app/models/booking.rb
Class Booking < ActiveRecord::Base
def is_past_due?
...logic here for instance method
end
end
Used find_or_create_by
You should only be referencing things once - it's called DRY (don't repeat yourself)
I did a poor job of asking this question. What I wanted to know was how to create the actual associations once everything is set up in the DB and Model files.
If you want to create a record of B that is in a many-to-many relationship with an existing record of A, it's the same syntax of A.Bs.create. What was more important for me, was how to link an A and B that already existed, in which case the answer was A.B_ids += B_id.
Two other things:
More obvious: if you created/ linked something one way, was the other way automatic? And yes, of course. In a many-to-many relationship, if you've done say A.B_ids += B_id, you no longer have to do 'B.A_ids += A_id`.
Less obvious: if A and B are joined by table AB, the primary key of table AB doesn't need to be added to A or B. Rails wants you to worry about the AB table as less as possible, so searches, builds, etc. can all be done by A.B or B.A instead of A.AB.B or B.AB.A

Traversing HABTM relationships on ActiveRecord

I'm working on a project for my school on rails (don't worry this is not graded on code) and I'm looking for a clean way to traverse relationships in ActiveRecord.
I have ActiveRecord classes called Users, Groups and Assignments. Users and Groups have a HABTM relationship as well as Groups and Assignments. Now what I need is a User function get_group(aid) where "given a user, find its group given an assignment".
The easy route would be:
def get_group(aid)
group = nil
groups.each { |g| group = g if g.assignment.find(aid).id == aid }
return group
end
Is there a cleaner implementation that takes advantage of the HABTM relationship between Groups and Assignments rather than just iterating? One thing I've also tried is the :include option for find(), like this:
def get_group(aid)
user.groups.find(:first,
:include => :assignments,
:conditions => ["assignments.id = ?", aid])
end
But this doesn't seem to work. Any ideas?
First off, be careful. Since you are using has_and_belongs_to_many for both relationships, then there might be more than one Group for a given User and Assignment. So I'm going to implement a method that returns an array of Groups.
Second, the name of the method User#get_group that takes an assignment id is pretty misleading and un-Ruby-like.
Here is a clean way to get all of the common groups using Ruby's Array#&, the intersection operator. I gave the method a much more revealing name and put it on Group since it is returning Group instances. Note, however, that it loads Groups that are related to one but not the other:
class Group < ActiveRecord::Base
has_and_belongs_to_many :assignments
has_and_belongs_to_many :users
# Use the array intersection operator to find all groups associated with both the User and Assignment
# instances that were passed in
def self.find_all_by_user_and_assignment(user, assignment)
user.groups & assignment.groups
end
end
Then if you really needed a User#get_groups method, you could define it like this:
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
def get_groups(assignment_id)
Group.find_all_by_user_and_assignment(self, Assignment.find(assignment_id))
end
end
Although I'd probably name it User#groups_by_assignment_id instead.
My Assignment model is simply:
class Assignment < ActiveRecord::Base
has_and_belongs_to_many :groups
end

Resources