There seem to be a number of ways to handle a multiple foreign key association. Each way I have approached this has their draw backs, and as I am new to Rails I am convinced others have come across a similar scenario and I am probably working on something solved long ago.
My question is:
What would be an efficient way of handling a multiple index key association, while still retaining all other Rails sql modifiers (such as :include etc)?
My scenario is:
I have a table association as follows (simplified), which is used to connect people to other people via links:
People
+----+-----------+
| id | name |
+----+-----------+
| 1 | Joe |
+----+-----------+
| 2 | Sally |
+----+-----------+
| 3 | Bob |
+----+-----------+
Links
+----+-----------+---------+
| id | origin_id | rcvd_id |
+----+-----------+---------+
| 1 | 2 | 1 |
+----+-----------+---------+
| 2 | 1 | 3 |
+----+-----------+---------+
| 3 | 3 | 2 |
+----+-----------+---------+
From row 1 of the above Links table, one can see that a Person (Sally = 2) is linked to another Person (Joe = 1).
It is easy for me to find all of a Persons Links if my foreign key was "origin_id". But this would only show People originating a Link. In my scenario I need to see all links regardless if they were originated or received by a Person. If for example I were to ask for all of Sally's links (Sally = 2), the result I would want would be:
Links
+----+-----------+---------+
| id | origin_id | rcvd_id |
+----+-----------+---------+
| 1 | 2 | 1 |
+----+-----------+---------+
| 3 | 3 | 2 |
+----+-----------+---------+
Hence I have 2 index keys, both "origin_id" and "rcvd_id".
One way this could be solved is with a Method:
class Person < ActiveRecord::Base
has_many :link_origins, :class_name => "Link", :foreign_key => :origin_id, :dependent => :destroy
has_many :link_rcvds, :class_name => "Link", :foreign_key => :rcvd_id, :dependent => :destroy
def links
origin_person + rcvd_person
end
However, this is not efficient. For example this requires the entire collection to be gathered from the database and only then does the paginate method work (I am using the will_paginate gem), which defeats the point as paginate should speed up the process by limiting the number of records called. Not limit the records after the entire collection is already done.
Also, the above will not allow me to call for example, Joe.links(:first).origin_id.name. Not exactly this code but meaning I could not call the Person details on the origin_id of a selected link, as the links method does not know that origin_id is related to the People table.
So far the most workable solution seems to be the :finder_sql.
class Person < ActiveRecord::Base
has_many :links, :finder_sql => 'SELECT * FROM links WHERE (links.origin_id = #{id} or links.rcvd_id = #{id})'
This gives all links where the Person_id matches either the Links.origin_id or the Links.rcvd_id.
The down side of this option, is that using :finder_sql, rules out all the other sql modifiers since Rails
doesn't know how to parse and modify the SQL you provide. For example I would not be able to use the :include option with the :finder_sql.
So, right now I am using the :finder_sql, solution. But it seems there might be a away of getting this association done in such a way that I don't need a :finder_sql. For example, is there a way to write a custom sql string while retaining the Rails sql modifiers that Active Record supplies.
Any ideas on the above?
I did find the solution to this, however it turned out I was probably asking the wrong question. I have not found away to have multiple index keys as I asked without implementing some custom sql which breaks different rails helpers.
I guess my question still stands, but how I did resolve this was to look at the problem differently. I just created the associations as they are:
belongs_to :rcvd_person, :class_name => 'Person', :foreign_key => :rcvd_id
belongs_to :origin_person, :class_name => 'Person', :foreign_key => :origin_id
And a custom sql statement:
class Person...
has_many :links, :finder_sql => 'SELECT * FROM links WHERE origin_id = #{id} OR rcvd_id = #{id}'
end
Then I was able to manipulate the records how I wanted in my view. In case anyone else is doing something similar, I did:
<% person.links.each do |link| %>
<% if link.origin_id == person.id %>
<%= link.rcvd_person.given_name %>
<% else %>
<%= link.origin_person.given_name %>
<% end %>
<% end %>
I'm not sure you can really support an association with multiple keys in the same table because Rails won't know which key to set if you attempt to create a relationship.
However, if you just want person.links, Rails 3 provides a way that is better than :finder_sql
class Link
def self.by_person(person)
where("origin_id => :person_id OR rcvd_id => :person_id", :person_id => person.id
end
end
class Person < ActiveRecord::Base
# You can include the has_many relationships if you want, or not
def links
Link.by_person(self)
end
end
This lets you do things like #person.links.limit(3) (which currently appears to be broken when using :finder_sql)
Related
I am developing a program for a warehousing/shipping company with the following Data relationships. The skinny regarding the relationships below is that the warehouse receives raw materials(product) from various carriers(clients) and stores them until they are needed to be shipped to the manufacturing plant. When a shipment leaves the warehouse, the manufacturing facility must know which company each raw material originated from. Take a look at the following ERD.
EDIT: My relationships in text form.
shipment.rb
has_many :product_shipments, :dependent => :destroy
has_many :products, :through => :product_shipments
product_shipment.rb
belongs_to :product
belongs_to :shipment
product.rb
has_many :product_shipments
has_many :shipments, :through => :product_shipments, :dependent => :destroy
belongs_to :client
client.rb
has_many :products, :dependent => :destroy
I'm having trouble generating the queries in a manner that's formatted the way the requirements demand. The shipment report takes a date and must iterate through each client and list the products shipped to the manufacturing facility on that given date. It needs to be generated dynamically and formatted like the following.
Shipment Date: 2013-01-01
Client: 12 Name: ACME Widget Co
Product Name | Product Code | Qty Shipped
_________________________________________
nuts & bolts | gj-3423 | 25
steel sheet | 4g-2394 | 10
dc Motor | cj-c213 | 4
Client: 14 Name: Blah Blah, Inc
Product Name | Product Code | Qty Shipped
_________________________________________
spacers | gj-3423 | 15
epoxy | 4g-2394 | 10
dc Motor | 24-gj19 | 6
Client: 15 Name: Sample Co, Inc
Product Name | Product Code | Qty Shipped
_________________________________________
hot roll 48 | cg-3423 | 15
welding wir | 4g-2394 | 10
dc Motor | c2-1241 | 6
.
.
.
.
The problem is generating Queries using ActiveRecord. It's easy to grab the shipments and products for a given date for ex below. It is not easy to grab the original client that the raw material originated from, then iterate through shipments to the manufacturing facility for that client dynamically.
UPDATE: I am now able to group clients and shipments like above. Now I need to exclude clients where they DON'T have a shipment on the date specified. The below, although somewhat correct, still gives the dreaded O(n) query. Here is what I have.
#clients = Client.includes(:products => :shipments)
#clients.each do |c|
puts c.name
c.products.each do |p|
p.shipments.where("ship_date like '#{#ship_date.strftime("%Y-%m-%d")}%'").each do |s|
s.product_shipments.joins(:product).each do |ps|
puts s.bill_of_lading + " " + ps.product_id.to_s + " " + ps.product.product_name.to_s + " " + ps.product.product_code + " " +ps.qty_shipped.to_s + " " + s.ship_date.to_s
end
end
end
end
My issue is how do I organize the query to start with clients and then list products shipped on '2012-06-30. The query gets wacko from that perspective. I am unsure how to generate a query with active record when the relationship is that far removed.
UPDATE: Ok looking at the results you expect in the report, values from ProductShipment (like the quantity attribute) need to be pulled out, so product_shipments must be included in the nested association we're eager loading, otherwise ProductShipments aren't instantiated, it only serves as a join table.
Therefore instead of Client.includes(:products => shipments)... you want :
#clients = Client.includes(:products => {:product_shipments => :shipment}).
where("shipments.ship='#{ship_date}'")
Now I don't fully understand your domain model, but when there's a lot of nested associations, I like to spot the ones which hold the most information in a one to one relationship, because they can be seen as center piece. In this case product and shipment can both be understood as extensions of the "master model" product_shipment.
Thus you can write (respectfully to Demeter's law) :
class ProductShipment < AR
def report_details
s = shipment; p = product
"#{s.bill_of_lading} #{p.id} #{p.name} #{p.code} #{quantity} #{s.shipped_on}"
end
end
Here comes the tricky part: as it is written :products => {:product_shipments => :shipment} Rails understands
product_shipments.shipment but not product_shipment.products
The later would actually trigger a db call... (which we're trying to avoid). Thankfully Rails has another trick in it's pocket :
class ProductShipment < ActiveRecord::Base
belongs_to :product, inverse_of: :product_shipments
end
class Product < ActiveRecord::Base
has_many :product_shipments, inverse_of: :product
end
Having insured the mirroring of associations you can now fetch product_shipments through products and get your report with no O(n) calls on the DB :
#client.map {|c| c.products.map {|p| p.product_shipments} }.flatten.each do |ps|
puts ps.details
end
UPDATE END
You must eager load the associated models or you get in the infamous O(n) query.
Shipment.where(:date => someday).includes({:product => :client}).each do |shipmt|
puts shipmt.date
shipmt.product.group_by(&:client).each do |client, products|
puts client
products.each do |product|
puts product.details
end
end
end
BTW rails does the join directly from shipment to product assuming you have a has_many :through or has_and_belongs_to_many association here, so no need to use the join table (aka product_shipment)
Here's how I did it. Maybe not the best way to do it but it's close. Thanks for #charlysisto for sending me in the right direction with easy loading the relationships. I still get a O(n) on the join query for the lookup table, but could be worse. Any refinements please comment so I can improve the answer.
#clients = Client.order("clients.id ASC").includes(:products => :shipments).where("shipments.ship_date like '#{ship_date}'")
#clients.each do |c|
puts c.name
c.products.each do |p|
p.shipments.each do |s|
s.product_shipments.joins(:product).each do |ps|
puts shipment and products stuff
end
end
end
Im working on a little app for Ruby on Rails and Ive got a question regarding the relationship between models.
I have "Teams" which play against each other in "Matches"
The tables currently look like this although I might have to make some changes to the matches table.
Teams:
id | Name
1 | some-name#1
2 | some-name#2
3 | some-name#3
Matches
id | team_id1 | team_id2 | result1 | result2
1 | 2 | 3 | -1 | -1
In this example the team with id 2 plays against team with id 3. The result is not yet entered and therefore set to "-1" for both. If Team 2 would loose against Team 3 with a score of "3:7" then result1 would be "3" and result2 "7"
A Team has many matches and one match belongs to two teams.
How can i model the relationships in Rails?
team.rb
has_many :matches
matches.rb
belongs_to :team_one, :foreign_key => "team_id1", :class_name => "Team"
belongs_to :team_two, :foreign_key => "team_id2", :class_name => "Team"
In this case suppose you want to find team one's name and team two's name from the object of matches do following
match = match.find(1)
match.team_one.name ### gives -> some-name#2
match.team_two.name ### gives -> some-name#3
I need some help building a table and then getting data from that table in Rails 3.
Here's the break down:
Models - 3 models involved here they are:
Thread has many participants
Participants belong to thread
Users
Activity table:
id | thread_id | participants
Example records would look something like:
1 | 300 | 3,1,5,67,13
2 | 333 | 3,12
3 | 433 | 1,12
4 | 553 | 1,12, 67
Where participants, is a list of user_ids, if there is a better way to store the user_ids please let me know. I haven't built this yet.
After I populate the activity table. I then want to be able to query along the lines of:
Select all Activity records where the participant_id of 67 is included in the participants field.
I hope the above is clear, if not please let me know. Ideas? Thoughts? Suggestions.
Thanks
While it's tempting to store multiple values in a column, it always ends up with someone getting hurt. You're better off building a join table to relate the models.
For example you could do this:
class DiscussionThread < ActiveRecord::Base
has_many :participations
has_many :participants, :through => :participations
end
class Participation < ActiveRecord::Base
belongs_to :discussion_thread
belongs_to :participant, :class_name => "User", :foreign_key => :user_id
end
class User < ActiveRecord::Base
has_many :participations
has_many :dicussion_threads, :through => :participations
end
That gives you three tables:
table: discussion_threads
columns: id
table: participations
columns: id | discussion_thread_id | user_id
table: users
columns: id
To find the threads in which a user is participating, just do:
#user.discussion_threads
And to find the users participating in a thread:
#discussion_thread.participants
Note: Thread is a reserved word in Ruby, so I've renamed it DiscussionThread
EDIT
mind showing an example of how to serialize an array of ids and then query against them?
You awaken in the middle of the night, and under the power of a strange compulsion you go to your computer and create this migration:
rails g model Abomination horror_ids:text
and model:
class Abomination < ActiveRecord::Base
serialize :horror_ids
end
You test it to make sure it can store an array:
#abomination = Abomination.create(:horror_ids=>[2,33,42])
#abomination.horror_ids # => [2,33,42]
So what? You know that behind the scenes Rails converts it to YAML, which looks like this:
---\n
- 2\n
- 33\n
- 42\n
Again compelled by that wierd urging, you wonder "How could I search for a particular id stored in this column?". Well, it's just a string, right? You know how to find that in a text field:
cthulhu = 33
Abomination.where(["horror_ids LIKE '%- ?\n%'",cthulhu]).first
With increasing dread, you realize someone might stumble across this and think it was actually a good idea. It must be destroyed! But you are unable to type rm -rf *, instead the strange force makes you consider the quirks that a future mindless follower of Cthulhu developer might need to know, such as
#abomination = Abomination.create
#abomination.horror_ids # => nil
#abomination = Abomination.create(:horror_ids=>[])
#abomination.horror_ids # => []
#abomination = Abomination.create(:horror_ids=>"any string value can go here")
#abomination.horror_ids # => "any string value can go here"
And the fact that serialized data can get corrupted when the column size is too small to accommodate it all.
You make one last ditch effort to kick out the power cord, but it is too late, the gibbering, insane consciousness that has taken control of you posts the code on StackOverflow for the whole world to see. Finally you collapse into a troubled sleep. The next day, realizing what you've perpetrated, you give up coding forever and become an accountant.
Moral
Don't do this
It's getting old rewriting report code for Rails applications. Ya have to create the query, the routes, the action, and the view...(yes, I'm lazy) Is there anything out there that would create a full report in even fewer steps?
Here's what I imagine would be ideal:
You have a Report record that has a name, query code (in Ruby or SQL), and perhaps some report options like so:
Report.create(:name => "With last name 'smith'",
:query => "Person.where( :last_name => 'smith' )")
This would store a record, and you'd dynamically get a route:
method : report_with_last_name_smith_path
http : GET
url : /report_with_last_name_smith
options : {
:controller => 'reports',
:action => 'with_last_name_smith'
}
And the report record would retrieve all columns from the query (which happens to be all
columns in the people table in this case), and generate a view with the data like so (pretend this is html):
| First Name | Last Name | Date of Birth | Sex |
| Bob | Smith | 03-13-2000 | Male |
| Lisa | Smith | 03-23-1980 | Female |
| Jack | Smith | 03-13-1975 | Male |
Anyone know of a plugin that helps achieve at least part of this?
By the way, the Ruport gem will likely be incompatible with Rails 3, and to be honest, it's a little unwieldy.
Here's something that gets us almost there:
http://github.com/wayneeseguin/dynamic_reports
In Dynamic Reports you create a report class that specifies a few parameters, and add a controller action to specify the query to use for the results.
Here's the example on the site:
# In some class that you create
class OrdersReport < DynamicReports::Report
title "Orders Report"
subtitle "All orders recorded in database"
columns :total, :created_at
link :total, '/report/item_sales?id={id}' # => Will substitute ID for the value of ID associated with that record
end
# In any controller of your choosing:
def orders
#orders = Order.find(:all, :limit => 25)
render :text => OrdersReport.on(#orders).to_html, :layout => "application"
end
The docs don't say anything about routes, so I assume we have to create those ourselves.
It also allows you to use any layout or custom templates you want, and it generates charts using the Google charts API.
In my projects i use ransack gem, it is amazing, it allow your user make custom queries, and your can customize the attributes available.
Take a look
Github
Railscast
I had a terrible morning. Lots of emails floating around about why things don't work. Upon investigating I found that there is a data mismatch which is causing errors.
Scenario
Customer and Address are two tables.
Customer contains
class Customer < ActiveRecord::Base
has_one :address, :foreign_key => "id"
end
Address Contains
class Address < ActiveRecord::Base
belongs_to :customer, :foreign_key => "cid"
end
So the two tables match on id which is the default and that column is auto incremented.
Problem
on the edit Page we have some code like this.
params[:line1] = #customer.first.address.line1
It fails because no matching record is found for a customer in the address table. I don't know why this is happening. It seems that over time a lot of records did not get added to Address table. Now problem is that when a new Customer is added (say with id 500) the Address will be added with some other id (say 425) ...now you don't know which address belongs to which customer.
Question
Being new to Rails, I am asking whether it is always considered good to create an extra column for joining of the records, rather than depending on the column that is automatically incremented? If I had a seperate column in Address table where I would manually insert the recently added customers id then this issue would not have come up.
That has_one-belongs_to relationship should result in the "belonging" model having the key of the "having" model. Or, in other words, the :foreign_key clause should be the same in both models.
If I have these:
class Customer < ActiveRecord::Base
has_one :address, :foreign_key => 'cid' # note foreign_key same as in Address
end
class Address < ActiveRecord::Base
belongs_to :customer, :foreign_key => 'cid' # note foreign_key same as in Customer
end
then I can do this:
>> cust = Customer.create(:name=>'Mr Custard')
+----+------------+
| id | name |
+----+------------+
| 1 | Mr Custard |
+----+------------+
1 row in set
>> add = cust.create_address(:line_1 => '42 Some Street', :line_2 => 'Some where')
+----+-----+----------------+------------+
| id | cid | line_1 | line_2 |
+----+-----+----------------+------------+
| 1 | 1 | 42 Some Street | Some where |
+----+-----+----------------+------------+
1 row in set
checking:
>> Customer.first.address
+----+-----+----------------+------------+
| id | cid | line_1 | line_2 |
+----+-----+----------------+------------+
| 1 | 1 | 42 Some Street | Some where |
+----+-----+----------------+------------+
1 row in set
>> Address.first.customer
+----+------------+
| id | name |
+----+------------+
| 1 | Mr Custard |
+----+------------+
and my database looks like this:
sqlite> select * from customers;
1|Mr Custard
sqlite> select * from addresses;
1|1|42 Some Street|Some where
(the nice table output for ActiveRecord results comes from Hirb, by the way)
The Rails' convention is for each table to have an auto-incrementing integer primary key column named id and additionally—in your example—for the addresses table to have a non auto-incrementing integer foreign key column named customer_id. As the name implies, this holds the primary key value from the associated record in the customers table.
If you follow these rules then there's no need to specify a :foreign_key option on the associations.