Composite Keys or Join Table - ruby-on-rails

I'm having trouble trying to figure out the best way to associate/join my tables 'teams' and 'schedules'. My models are:
class Team < ActiveRecord::Base
attr_accessible :city, :conf, :div, :key, :name
self.primary_key = "key" #Abbreviations for strings 'CHI', 'TB', 'SEA' etc.
has_many :schedules, foreign_key: [:away_team, :home_team]
end
class Schedule < ActiveRecord::Base
attr_accessible :away_team, :date, :home_team, :season, :week, :team_key
self.primary_keys = :away_team, :home_team #Abbreviations for strings 'CHI', 'TB', 'SEA' etc.
belongs_to :team, primary_key: "key"
end
I installed the gem "composite_primary_keys", "~> 5.0.13".
In rails console when I assign a variable
>> team = Team.find("SEA")
SELECT "teams".* FROM "teams" WHERE "teams"."key" = ? LIMIT 1 [["key", "SEA"]]
=> #<Team key: "SEA", city: "Seattle", name: "Seahawks", conf: "NFC", div: "West">
it displays Seattle perfectly, but when I run:
>> team.schedules
SELECT "schedules".* FROM "schedules" WHERE ("schedules"."away_team" = 'SEA' AND "schedules"."home_team" IS NULL)
=> []
The 'schedule' table has data for 'SEA' in the home_team column.
Would a join table be a better solution? If so, how would you do it? Any help would be greatly appreciated!

First: don't use composit keys, if you don't have to (and you don't in this case).
Primary keys are for accessing individual items, not for constraints or business logic.
In your case, the primary index can't be used to select schedules by home_team.
Also it's a good idea to use Rails conventions as much as possible. It makes life easier.
Use id as primary key and for joining tables.
I see that your schedule belongs to two teams, and team have two kinds of schedules (home and away). So you models could look like:
class Team < ActiveRecord::Base
attr_accessible :city, :conf, :div, :key, :name
has_many :home_schedules, class_name: 'Schedule', foreign_key: :home_team_id
has_many :away_schedules, class_name: 'Schedule', foreign_key: :away_team_id
def schedules
home_schedules + away_schedules
end
end
class Schedule < ActiveRecord::Base
attr_accessible :away_team, :date, :home_team, :season, :week, :team_key
belongs_to :home_team, class_name: 'Team'
belongs_to :away_team, class_name: 'Team'
end
then
team = Team.find_by_key('SEA')
team.schedules
Added
I would generate the model with
rails g model Team city conf div short_cut name
rails g model Schedule day:date season week:integer home_team_id:integer away_team_id:integer
You link teams to fans by adding team_id to Fan. You can always use the team's short_cut (as I named your former key attribute) to display or select the team, but let Rails use id for all the internal stuff.

Related

rails_admin search through nested belongs_to association

I have a model:
class TenantReference < ActiveRecord::Base
include TenantReferenceAdmin
belongs_to :tenant, inverse_of: :reference
default_scope { eager_load(:tenant) }
end
and a Tenant model:
class Tenant < ActiveRecord::Base
default_scope { eager_load(:user) }
belongs_to :user
end
and a User model:
class User < ActiveRecord::Base
has_many :tenants, :foreign_key => :user_id, class_name: 'Tenant'
end
and finally a TenantReferenceAdmin rails admin file:
module TenantReferenceAdmin
extend ActiveSupport::Concern
included do
rails_admin do
list do
field :tenant do
filterable true
queryable true
searchable [ :first_name, :last_name]
end
...
what I'm trying to achieve is that in the tenantreference admin page the user can search TenantReference objects by the first_name or last_name of the user through their Tenant reference.
This configuration is producing a postgresql query like:
SELECT "tenant_references"."id" AS t0_r0, .... "tenants"."id" AS t1_r0, ......
FROM "tenant_references" LEFT OUTER JOIN "tenants" ON "tenants"."id" = "tenant_references"."tenant_id"
WHERE ((tenants.first_name ILIKE '%query%')
OR (tenants.last_name ILIKE '%query%')
ORDER BY tenant_references.id desc LIMIT 20 OFFSET 0)
which doesn't work, because first/last_name are actually fields of user, not of tenant.
how could I fix this?
Thanks,
The problem seems to be that rails admin only adds JOIN to the query if the current model has a direct link (has_many, has_one ...) to the other model to search in. and it joins it if the corresponding field is marked as queryable true.
so I changed adding to the references model this line:
has_one :user, through: :tenant
I then created an invisible list field:
field :user do
visible false
queryable true
searchable [{User => :first_name}, {User => :last_name}]
end
that can be searched upon, but it's not shown in the list.
this has solved the issue, I don't consider this ideal as I had to modify my model in order to be able to perform rails_admin search, when rails_admin could have handled this situation without changing the code. But for now I can live with this

Seeding a database with "flights"

I'm trying to seed my database and I keep getting the error "ActiveRecord::RecordInvalid: Validation failed: Arriving flight must exist". In my method to create the association in my seeds.rb file I supply the arrival_airport_id so I'm not sure what the problem is.
seeds.rb
Airport.delete_all
Flight.delete_all
#Airport seeds
airports = [
["Boston Logan International Airport", "BOS"],
["Gulfport", "GPT"],
["Jackson", "JAN"],
["Charleston", "CRW"]
]
airports.each do |full_name, name|
Airport.create!( full_name: full_name, name: name )
end
a = Airport.all[0..1]
b = Airport.all[2..3]
a.each_with_index do |a, index|
a.departing_flights.create!(
arrival_airport_id: b[index]
)
end
Airport model:
class Airport < ApplicationRecord
has_many :departing_flights, class_name: "Flight", foreign_key: "departing_airport_id"
has_many :arriving_flights, class_name: "Flight", foreign_key: "arrival_airport_id"
end
Flight model:
class Flight < ApplicationRecord
belongs_to :departing_flight, class_name: "Airport", foreign_key: "departing_airport_id"
belongs_to :arriving_flight, class_name: "Airport", foreign_key: "arrival_airport_id"
end
This is a common mistake and there are two fixes.
a.each_with_index do |a, index|
a.departing_flights.create!(
arrival_airport_id: b[index] # This line is the problem
)
end
You're assigning an object to an id column. You can either assign the id to the id column or the object to the object column.
arrival_airport_id: b[index].id
# or
arrival_airport: b[index]
Rails tries to help you out the best it can but you have to give it the right object types.

How to find records, whose has_many through objects include all objects of some list?

I got a typical tag and whatever-object relation: say
class Tag < ActiveRecord::Base
attr_accessible :name
has_many :tagazations
has_many :projects, :through => :tagazations
end
class Tagazation < ActiveRecord::Base
belongs_to :project
belongs_to :tag
validates :tag_id, :uniqueness => { :scope => :project_id }
end
class Project < ActiveRecord::Base
has_many :tagazations
has_many :tags, :through => :tagazations
end
nothing special here: each project is tagged by one or multiple tags.
The app has a feature of search: you can select the certain tags and my app should show you all projects which tagged with ALL mentioned tags. So I got an array of the necessary tag_ids and then got stuck with such easy problem
To do this in one query you'd want to take advantage of the common double not exists SQL query, which essentially does find X for all Y.
In your instance, you might do:
class Project < ActiveRecord::Base
def with_tags(tag_ids)
where("NOT EXISTS (SELECT * FROM tags
WHERE NOT EXISTS (SELECT * FROM tagazations
WHERE tagazations.tag_id = tags.id
AND tagazations.project_id = projects.id)
AND tags.id IN (?))", tag_ids)
end
end
Alternatively, you can use count, group and having, although I suspect the first version is quicker but feel free to benchmark:
def with_tags(tag_ids)
joins(:tags).select('projects.*, count(tags.id) as tag_count')
.where(tags: { id: tag_ids }).group('projects.id')
.having('tag_count = ?', tag_ids.size)
end
This would be one way of doing it, although by no means the most efficient:
class Project < ActiveRecord::Base
has_many :tagazations
has_many :tags, :through => :tagazations
def find_with_all_tags(tag_names)
# First find the tags and join with their respective projects
matching_tags = Tag.includes(:projects).where(:name => tag_names)
# Find the intersection of these lists, using the ruby array intersection operator &
matching_tags.reduce([]) {|result, tag| result & tag.projects}
end
end
There may be a couple of typos in there, but you get the idea

Multiple belongs_to to the same table

I have two tables:
currencies and rates
currencies: id:int, code:string, name: string
rates: id:int, top_currency_id:int, bottom_currency_id:int, rate:float
And I have two active records for them:
class Rate < ActiveRecord::Base
attr_accessible :bottom_currency, :rate, :top_currency, :top_currency_id
belongs_to :top_currency, :class_name => 'Currency', :foreign_key => 'top_currency_id'
belongs_to :bottom_currency, :class_name => 'Currency', :foreign_key => 'bottom_currency_id'
end
class Currency < ActiveRecord::Base
attr_accessible :code, :name
has_many :rates
end
So the problem is:
When I'm tring to execute following code:
top_currency = Currency.find_by_id(1)
#test = Rate.where(:top_currency=>top_currency)
I getting following error:
Mysql2::Error: Unknown column 'rates.top_currency' in
'where clause': SELECT `rates`.* FROM `rates` WHERE `rates`.`top_currency` = 1
Why Rails's magic doesn't work?
Many thanks.
In your two belongs_to methods, change the foreign_key option to primary_key, leaving everything else as is.
belongs_to :top_currency, :class_name => 'Currency', :primary_key => 'top_currency_id'
# ...
By default, an associated object's primary key is id. However, your currency model has three primary keys, the expected id plus two extra keys: top_currency_id and bottom_currency_id. Active Record needs to know which key to look for. Tell it with the primary_key option.
The foreign_key option is needed when a foreign key is different than the association's name (belongs_to :name) plus "_id". Since your foreign key matches the association name plus "_id," you do not need to use the foreign_key option.
From what I see, your code should work in theory. But I do think you are being a bit redundant.
It should be enough to just do this:
class Rate < ActiveRecord::Base
belongs_to :top_currency, class_name: 'Currency'
belongs_to :bottom_currency, class_name: 'Currency'
end
Rails will infer that the foreign key for top_currency is top_currency_id, and bottom_currency_id for bottom_currency.
I don't think you can query on the relationship like that. To use your example:
top_currency = Currency.find_by_id(1)
#test = Rate.where(:top_currency=>top_currency)
You'd have to change it to this:
top_currency = Currency.find_by_id(1)
#test = Rate.where(:top_currency_id => top_currency.id)
But it might just be easier to do this:
top_currency = Currency.find_by_id(1)
#test = top_currency.rates

How to make a has_one associated model show up in class attributes() result?

Where Person has_one Brain, should I expect brain to show up in the hash returned by attributes() for Person? If so, how to make that happen?
Rails Console output:
1.9.3p327 :003 > Person.new.attributes
=> {"id"=>nil, "name"=>nil, "created_at"=>nil, "updated_at"=>nil}
1.9.3p327 :004 > Brain.new.attributes
=> {"id"=>nil, "weight_kg"=>nil, "created_at"=>nil, "updated_at"=>nil, "person_id"=>nil}
The two models are Person and Brain:
class Person < ActiveRecord::Base
has_one :brain
attr_accessible :name
attr_accessible :brain
attr_accessible :brain_attributes
accepts_nested_attributes_for :brain
end
class Brain < ActiveRecord::Base
belongs_to :person
attr_accessible :weight_kg
attr_accessible :person
attr_accessible :person_attributes
accepts_nested_attributes_for :person
end
person_id shows up in the attributes for Brain because the brains table has a person_id column. No such column need exist for the people table.
not sure it is a good idea to list associations among attributes, but you can get association names like this:
association_names = self.class.reflect_on_all_associations.map(&:name)
see doc on reflections
It doesn't show attributes for brain in person because if you look at the sql table for person .. there is no field for "brain" .. but in the brain table there is person_id.
Good tip from Viktor on how to reflect on associations!

Resources