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
Related
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.
I have two models: player and team.
A player has one team
Team has 5 fields on it (in addition to its name and location), opponent_week_1, opponent_week_2, etc.
I would like to be able to say something like Player.Team.opponent_week_1
How should I relate the models to each other? Player has_one team?
How do I set the team's opponents? I don't want the team to have_many opponents, because there will only be those 5, and I want to be able to say opponent_week_1, opponent_week_2, etc.
I am using Ruby 2 and Rails 4. Thanks!
jackerman09,
As with many things in Rails, there's a few ways of going about it. #phgrey pointed out how to fix the players and teams.
Regarding opponent_week_1, 2, etc.:
I think the best way is if you actually do have a has_may :opponent_week association from the Team model, like this:
class Team < ActiveRecord::Base
...
has_many :fields
...
end
You'd then have to limit insertion of opponent weeks to each Team to only 5 through validations and/or through the forms. Since users will be entering those opponent weeks through forms, that would be an easy way to go about it at first. You have control of the forms, so just limit how many opponent_weeks they put in for each Team through forms.
How you'd go about about calling them opponent_week_1, opponent_week_2 etc.: there are a few ways. I would try putting a a method_missing method in your model (google to see how to do it) then parse the name of the method that you called. Something like this:
def method_missing( method_name )
if method_name.starts_with?( "opponent_week_" )
# get the number at the end, then call
opponent_weeks[ num_of_week - 1 ]
else
super
end
end
All the best and let me know if you need clarifications.
Take a look here - http://guides.rubyonrails.org/association_basics.html
1. Players <=> Team
class Player < ActiveRecord::Base
...
belongs_to :team
...
end
class Team < ActiveRecord::Base
...
has_many :players
...
end
Be sure that migration create_players has field
t.references :team_id
2. Oppenents
Youre lead the worsest way. Better take a look at HABTM (has_and_belongs_to_many) relations between commands
Kind of new to Ruby/Rails, coming from c/c++, so I'm doing my baby steps.
I'm trying to find the most elegant solution to the following problem.
Table A, among others has a foreign key to table B (let's call it b_id), and table B contains a name field and a primary (id).
I wish to get a list of object from A, based on some criteria, use this list's b_id to access Table B, and retrieve the names (name field).
I've been trying many things which fail. I guess I'm missing something fundamental here.
I tried:
curr_users = A.Where(condition)
curr_names = B.where(id: curr_users.b_id) # fails
Also tried:
curr_names = B.where(id: curr_users.all().b_id) # fails, doesn't recognize b_id
The following works, but it only handles a single user...
curr_names = B.where(id: curr_users.first().b_id) # ok
I can iterate the curr_users and build an array of foreign keys and use them to access B, but it seems there must be more elegant way to do this.
What do I miss here?
Cheers.
Assuming you have following models:
class Employee
belongs_to :department
end
class Department
has_many :employees
end
Now you can departments based on some employee filter
# departments with employees from California
Department.include(:employees).where(:employees => {:state => "CA"}).pluck(:name)
For simplicity, let's take an example of Article and Comments, instead of A and B.
A Comment has a foreign key article_id pointing at Article, so we can setup a has_many relationship from Article to Comment and a belongs_to relationship from Comment to Article like so:
class Article < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :article
end
Once you have that, you will be able do <article>.comments and Rails will spit out an array of all comments that have that article's foreign key. No need to use conditionals unless you are trying to set up a more complicated query (like all comments that were created before a certain date, for example).
To get all the comment titles (names in your example), you can do <article>.comments.map(&:title).
I am working on a Ruby on Rails 3 web application and am not sure how to relate two of the models.
In our organization sales reps go out on appointments. If the appointment is successful, it will result in creating an order (which then has the items ordered related to it, but that's for another day.) If this appointment is not successful, it will be marked as no sale and as you might have guessed, no order is created.
On the other hand, sometimes sales happen without an appointment. For example, a customer may call into the store and order something. In this case, an order can exist without an appointment.
It would be simple if there were no relationship between orders and appointments, but there has to be for ease of use for the end user. For example, if an appointment generates an order, but later the buyer cancels, they will mark the appointment as sale cancelled and then the system should automatically set the order as cancelled. Likewise,they may choose to cancel the order, then the appointment would have to be cancelled automatically by the system.
How does a developer handle something like this? Does the appointment :have_many => orders? does the order :belong_to => appointments? I don't know what to do!
Please help me with this, I am a pretty new rails developer and I feel in over my head! Thank you!
As you already said, the following will work fine:
class Appointment < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :appointment
end
belongs_to requires the field appointment_id to be present in the orders table. But, if the order is not associated with an order then appointment_id does not need to be set. You can have multiple belongs_to associations for a given class.
This is my first post on Stack, so please bear with me if I breach any protocol.
I'm working on a project in Rails (2.1.2), and I have a relations scenario that looks like this:
event [has_many] days
People (in different categories) can sign up for event days, giving the following binding results:
category [has_many] customers [has_many] orders [has_many] days
[belongs_to] event
Now, I'd like to have the total number of 'events' for one customer, or for all customers in a certain category, and I'm stuck. AFAIK, there's no way of performing a simple 'find' through an array of objects, correct? So, what would be the solution; nested loops, and a collect method to get the 'events' from the 'days' in orders?
Please let me know if I'm unclear.
Thanks for your help!
I would personally do this using a MySQL statement. I don't know for sure, but I think it is a lot faster then the other examples (using the rails provided association methods).
That means that in the Customer model you could do something like:
(Note that I'm assuming you are using the default association keys: 'model_name_id')
class Customer
def events
Event.find_by_sql("SELECT DISTINCT e.* FROM events e, days d, orders o, customers c WHERE c.id=o.customer_id AND o.id=d.order_id AND e.id=d.event_id")
end
end
That will return all the events associated with the user, and no duplicated (the 'DISTINCT' keyword makes sure of that). You will, as with the example above, lose information about what days exactly the user signed up for. If you need that information, please say so.
Also, I haven't included an example for your Category model, because I assumed you could adapt my example yourself. If not, just let me know.
EDIT:
I just read you just want to count the events. That can be done even faster (or at least, less memory intensive) using the count statement. To use that, just use the following function:
def event_count
Event.count_by_sql(SELECT DISTINCT COUNT(e.*) FROM ... ... ...
end
Your models probably look like this:
class Category
has_many :customers
class Customer
has_many :orders
has_many :days, :through => :orders # I added this
belongs_to :category
class Order
has_many :days
belongs_to :customer
class Day
belongs_to :event
belongs_to :order
class Event
has_many :days
With this you can count events for customer:
events = customer.days.count(:group => 'event_id')
It will return OrderedHash like this:
#<OrderedHash {1=>5, 2=>13, 3=>0}>
You can get events count by:
events[event_id]
events[1] # => 5
events[2] # => 13
etc.
If you want total number of uniq events:
events.size # => 3
In case of counting events for all customers in category I'd do something like this:
events = {}
category.customers.each {|c| events.merge!(c.days.count(:group => 'event_id') ) }
events.size # => 9 - it should be total number of events
But in this case you lose information how many times each event appeared.
I didn't test this, so there could be some mistakes.