I'm trying to create a Transaction that simultaneously is the child of a Request and also one part in a many-to-many relationship of an Inventory.
Model code:
class Transaction < ActiveRecord::Base
belongs_to :request
has_many :transactories
has_many :inventories, through: :transactories
end
class Inventory < ActiveRecord::Base
has_many :transactories
has_many :transactions, through: :transactories
end
class Transactory < ActiveRecord::Base
belongs_to :inventory
belongs_to :transaction
end
Here's the flow I'm trying to achieve:
User POSTs a request that contains additional data in a hash where key = the itemlist_id of what they want and the value = the quantity of that itemlist_id that they want. Let's say user wants two of 9, that would look like this: { 9 => 2}
For each itemlist_id in the user provided hash, I'm going to look inside the Inventories table and pull out the inventory_ids where the itemlist_id matches what the user is looking for and the owner of that inventory_id is not the user him or herself. Let's say that in the Inventories table, there are 3 ids that fulfill this: [X, Y, Z]
Now what I'd like to do is create the Transactions that belong to the Request (Request was already created earlier) and associate the Transactions and Inventories with each other. The outcome of this step is two-fold (I think it's easier to write from the perspective of what the view will look like:
that the owner of each X, Y, and Z inventory_id should see that there are 2 transactions for their item (so they can pick which one they want to honor)
that the user can see that for each of their 2 transactions, there are notifications to the owners of each X, Y, and Z
Code to create the associations
# Assume overarching parent request has been created, called #requestrecord
# Step 1, #transactionparams = { 9 => 2 }
#transactionparams.each do |itemlist_id, quantity|
# Step 2 matched_inventory_id = [X,Y,Z]
matched_inventory_id = Inventory.where.not(signup_id: #requestrecord.signup.id).where(itemlist_id: itemlist_id).ids
# Step 3, 2 transactions created each with itemlist_id of 9, each associated with inventory_ids X, Y, Z. In turn, inventory_ids X, Y, Z each associated with each of the two transactions created
quantity.to_i.times do
transaction = #requestrecord.transactions.create(itemlist_id: itemlist_id)
transaction.inventories.create matched_inventory_id
end
end
The line I can't get right is in step 3:
transaction.inventories.create matched_inventory_id
This throws an error that the parameters for create must be a hash. I also tried:
matched_inventory_id.each do |id|
transaction.inventories.create(inventory_id: id)
end
This failed because inventory_id is not a valid attribute. So... two questions:
How do I associate each of X, Y, Z inventory id with each transaction 1 and 2?
If I write one line of code to achieve above, conceivably (hopefully), I've achieved the reverse association as well? Meaning in a has_many :through, as long as I associate Inventory with Transactions, I'm automatically also associating Transactions with Inventories, right?
Finally someone answered this question here: Creating joined records using has_many :through
Basically I created the Transaction belonging to Request parent first, then associated it with the Inventories like so:
transaction.inventory_ids += matched_inventory_ids
That new line replaced this old line of code:
transaction.inventories.create matched_inventory_id
And yes, once it's associated one way, the two-way works as well since it's a many-to-many relationship.
Related
My Classroom model has two attributes, a student_classcode and a teacher_classcode. I have an after_create callback to generate these two codes after the classroom is created. Currently, I only generate the student_classcode so far with this:
(taken from here)
class Classroom < ActiveRecord::Base
after_create :generate_token
private
MAX_RETRIES = 10
def generate_token
update_column :student_classcode, SecureRandom.hex(4)
rescue ActiveRecord::RecordNotUnique => e
#token_attempts = #token_attempts.to_i + 1
retry if #token_attempts < MAX_RETRIES
raise e, "Retries exhausted"
end
end
Right now it makes sure that the student_classcode is unique (with up to 10 retries). I want to be able to generate a teacher_classcode as well, and I want to make sure that it is unique among the teacher_classcode column and the student_classcode column.
So for example, if Classroom A has the student_classcode '12345', and (by chance) the teacher_classcode of Classroom B generates to '12345', I want Classroom B to regenerate the classcode. Or, if Classroom B has the student_classcode 'abcde', and Classroom B has the teacher_classcode 'abcde', then I want Classroom B to regenerate the teacher_classcode.
I know that the chances of there being two of the same classcode across both columns are small, but I don't want to take the chances. How can I do that?
Why don't you make Classcode a model in its own right, with validations to ensure the code is unique. Then generate a generic code and through standard belongs_to associations from the Classroom you can associate a teacher and a student classcode.
This way you can be sure each classcode is unique and only have one implementation of code generation.
For now, I've got Three Models
# town.rb
class Town < ActiveRecord::Base
has_many :buildings
end
# building.rb
class Building < ActiveRecord::Base
belongs_to :town
end
# building_default.rb
class BuildingDefault < ActiveRecord::Base
end
I want the following to happen when a User creates a Town :
Populate the user's Building model with records based upon the
information contained in the BuildingDefault model.
Set each building.town_id correctly.
For example, lets assume Building and BuildingDefault have the attribute :name in common with each other. And BuildingDefault contains two records (it will actually contain ~ 125):
BuildingDefault.all
# => <ActiveRecord::Relation [#<BuildingDefault id: 1, name: "cannon">, #<BuildingDefault id: 2, name: "archer">]>
Then a User fills out a form that creates a new Town. I want to do an after_create method which copies everything from BuildingDefault to Building. In this case Building would end up with:
Building.find_by_town_id(1)
# => <ActiveRecord::Relation [#<Building id: 69, town_id: 1, name: "cannon">, #<Building id: 70, town_id: 1, name: "archer">]>
What's a possible way to facilitate this behavior?
I think this should be enough :
after_create :set_buildings
private
def set_buildings
BuildingDefault.all.each do |default_b|
buildings.create(id: default_b.id, name: default_b.name)
end
end
I am not sure if you are confusing with user_id and town_id or you really want user model also to be associated with building/town. If it's the latter, please update your code to include with a 'belongs_to' user association.
For now, I am assuming you want to associate towns and buildings only. It is a case of many_to_many relationships. In that case, there are two options I see.
If all your buildings have no special attribute related to individual town,(i.e, building of type "cannon" has the same values for Town A or Town B), you can just associate has_and_belongs_to_many relationship between the two creating a dummy join table.Then you can add all buildings to each town when it is created by following code:
#town.buildings = Building.all
#town.save!
If there can be different values related to buildings for individual town, then you can set up a has_many buildings, through: :building_town relationship in Town model and puts those differing attributes on the intermediate model which in this case is BuildingTown.
I don't see the need for keeping a DefaultBuilding model if all you do is copy all over to the actual Building model.
If I understand has_one correctly, the foreign key is on the associated record. This means that I could have multiple related records on the "foreign" side. When I use the has_one records to get the associated record, how do I determine which one will be returned?
Here's an example:
class Job < ActiveRecord::Base
has_one :activity_state
end
class ActivityState < ActiveRecord::Base
belongs_to :job
end
Let's say there are 2 rows in the activity_states table that have their foreign key set to Job #1
ActivityState.where(job_id: 1).select(:id, :job_id)
#=> [#<ActivityState id: 1, job_id: 1>, #<ActivityState id: 2, job_id: 1]
When I try to get the activity_state from the JobRequest, how do I determine which one is returned?
JobRequest.find(1).activity_state
#=> #<ActivityState id: 1, job_id: 1>
Right now, it looks like it is returning the first one it finds.
What if I wanted to return the latest one that was created?
As discussed in the comments, the main issue is that your system is creating multiple ActivityState objects for each job. As you mentioned, your original code was doing this:
ActivityState.create(job_id: #job.id)
The problem is that the old ActivityState still contains the job_id for that job. Consequently, when you executed #job.activity_state, you were indeed seeing the first (and old) activity state because it is the first in the database. Specifically, the has_one relationship executes a LIMIT 1 sql clause, so technically the "first" record is dependent on however your database was ordering the records (usually by order of entry)
Normally, for a has_one relationship, if you were to do
#job.activity_state = ActivityState.new(...activity state params...)
Rails attempts to enforce the "one association per record" concept by resetting the "old" associated record's job_id column to null. Your original code was unintentionally circumventing that enforcement. If you change it to this line of code instead, you will allow Rails to work its magic and ensure consistent behaviour with your association.
You should have a has_many association in this case. To return a particular record based on a column, create a method.
class Job < ActiveRecord::Base
has_many :activity_states
def latest_activity_state
self.activity_states.order("updated_at ASC").last # or created_at, depends
end
end
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
I have an active record association below
class Order < ActiveRecord::Base
has_many :lineitems, dependent: :destroy
end
class Lineitem < ActiveRecord::Base
belongs_to :order
end
I have 2 Orders with different line items.
I want to merge these 2 orders to a new order and then delete the previous 2 records.
Is there an easy way to accomplish this, without creating new lineitems?
What I want is the same lineitems pointing to the new Order. Instead of making new copies.
You can do it by this way:
Create new order
new_order=Order.create #Pass additional parameters to create method if needed
Take all LineItem which is belongs to other two orders. E.g. orders with ids 1 and 2.
And attach all LineItems to new_order:
LineItem.where(order_id: [1, 2]).update_all(order_id: new_order.id)
Delete old orders
Order.where(id: [1, 2]).destroy_all
o1 = Order.find(1)
o2 = Order.find(2)
# create new one, and assign lineitems to the new order
o3 = Order.create(...)
o3.lineitems = o1.lineitems + o2.lineitems
# destroy the old order
o1.destroy
o2.destroy