I think I have followed the rails naming conventions in my app. But when I am in the terminal testing code I am coming across some errors that go against naming conventions. Here is my terminal session:
irb(main):010:0> a = _
=> #<Neighborhood id: 24, name: "Lincoln Park", created_at: "2011-12-03 20:29:00", updated_at: "2011-12-03 21:08:47", minlat: 41.91092, maxlat: 41.925658, minlng: -87.648761, maxlng: -87.636117>
irb(main):011:0> a.cta_trains
NoMethodError: undefined method `cta_trains' for #<Neighborhood:0x007fd666ee61e8>
from /usr/local/Cellar/ruby/1.9.2-p290/lib/ruby/gems/1.9.1/gems/activemodel-3.1.1/lib/active_model/attribute_methods.rb:385:in `method_missing'
Now when I try a.CtaTrains:
irb(main):012:0> a.CtaTrains
CtaTrain Load (0.4ms) SELECT "cta_trains".* FROM "cta_trains" INNER JOIN "cta_locations" ON "cta_trains"."id" = "cta_locations"."CtaTrain_id" WHERE "cta_locations"."neighborhood_id" = 24
SQLite3::SQLException: no such column: cta_locations.CtaTrain_id: SELECT "cta_trains".* FROM "cta_trains" INNER JOIN "cta_locations" ON "cta_trains"."id" = "cta_locations"."CtaTrain_id" WHERE "cta_locations"."neighborhood_id" = 24
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: cta_locations.CtaTrain_id: SELECT "cta_trains".* FROM "cta_trains" INNER JOIN "cta_locations" ON "cta_trains"."id" = "cta_locations"."CtaTrain_id" WHERE "cta_locations"."neighborhood_id" = 24
From my models:
class Neighborhood < ActiveRecord::Base
has_many :cta_trains, :through => :cta_locations
has_many :cta_locations, :foreign_key => :neighborhood_id
end
class CtaTrain < ActiveRecord::Base
has_many :neighborhoods, :through => :cta_locations
has_many :cta_locations, :foreign_key => :cta_train_id
end
class CtaLocation < ActiveRecord::Base
belongs_to :neighborhood
belongs_to :cta_train
end
I am at a standstill, stuck, banging my head against the wall, etc. Any help would be fabulous.
Rails noobie here....as if this point is not obvious.....
Noticed that you appear to be in IRB... Instead, I'd try to stay in the rails console when working with your active-record classes.
so start that with
bundle exec rails console
What you need here is a junction table. See the association has_and_belongs_to_many.
The junction table will store the links between a certain Neighbourhood and a certain CtaTrain. Here, it's CtaLocation but if you don't plan to actually use this model, you could even not define it.
For instance, you can achieve it With three tables (neighbourhoods, cta_trains and cta_trains_neighbourhoods) and only two models like :
class Neighbourhood
has_and_belongs_to_many :cta_trains
end
class CtaTrain
has_and_belongs_to_many :neighbourhoods
end
Related
I am trying to build a simple thesaurus app in Rails, in which a word in a table of words would be in a has-many, self-joined relationship to other words in the table, through a joiner table of synonym-pairs.
My SynonymPair class is built as follows:
class SynonymPair < ActiveRecord::Base
belongs_to :word1, class_name: :Word
belongs_to :word2, class_name: :Word
end
A crucial aspect of this thesaurus program is that it should not matter whether a word is in the word1 or word2 column; word1 is a synonym of word2, and vice versa.
In order for my Words class to return the SynonymPairs and Synonyms of a given word, I wrote a SQL query:
class Word < ActiveRecord::Base
def synonym_pairs
#joins :synonym_pairs and :words where either word1_id OR word2_id matches word.id.
sql = <<-SQL
SELECT synonym_pairs.id, synonym_pairs.word1_id, synonym_pairs.word2_id, words.word FROM synonym_pairs
JOIN words ON synonym_pairs.word1_id = words.id WHERE words.word = ?
UNION SELECT synonym_pairs.id, synonym_pairs.word1_id, synonym_pairs.word2_id, words.word FROM synonym_pairs
JOIN words ON synonym_pairs.word2_id = words.id WHERE words.word = ?
SQL
#returns synonym_pair objects from the result of sql query
DB[:conn].execute(sql,self.word,self.word).map do |element|
SynonymPair.find(element[0])
end
end
def synonyms
self.synonym_pairs.map do |element|
if element.word1 == self
element.word2
else
element.word1
end
end
end
end
This code works as intended. However, it does not take advantage of association models in ActiveRecord. So, I was wondering it would be possible to write a has_many :synonyms_pairs/has_many :synonyms through: :synonym-pairs custom relation query in the Words class, rather than writing out an entire SQL query, as I did above. In other words, I'm curious if it's possible to convert my SQL query into a Rails custom relations query.
Note, I tried the following custom relations query:
class Word < ActiveRecord::Base
has_many :synonym_pairs, ->(word) { where("word1_id = ? OR word2_id = ?", word.id, word.id) }
has_many :synonyms, through: :synonym_pairs
end
But, after passing a few Word/SynonymPair seeds, it returned a 'ActiveRecord:Associations:CollectionProxy' when I tried getting I called word#synonym_pairs and the following error when I called word#synonyms:
[17] pry(main)> w2 = Word.create(word: "w2")
=> #<Word:0x00007ffd522190b0 id: 7, word: "w2">
[18] pry(main)> sp1 = SynonymPair.create(word1:w1, word2:w2)
=> #<SynonymPair:0x00007ffd4fea2230 id: 6, word1_id: 6, word2_id: 7>
[19] pry(main)> w1.synonym_pairs
=> #<SynonymPair::ActiveRecord_Associations_CollectionProxy:0x3ffea7f783e4>
[20] pry(main)> w1.synonyms
ActiveRecord::HasManyThroughSourceAssociationNotFoundError: Could not find the source association(s) "synonym" or :synonyms in model SynonymPair. Try 'has_many :synonyms, :through => :synonym_pairs, :source => <name>'. Is it one of word1 or word2?
Any other ideas for getting a custom relation query, or any sort of self-join model working here?
Instead of a table of synonym pairs you can just create a standard M2M join table:
class Word
has_many :synonymities
has_many :synonyms, though: :synonymities
end
class Synonymity
belongs_to :word
belongs_to :synonym, class_name: 'Word'
end
class CreateSynonymities < ActiveRecord::Migration[6.0]
def change
create_table :synonymities do |t|
t.belongs_to :word, null: false, foreign_key: true
t.belongs_to :synonym, null: false, foreign_key: { to_table: :words }
end
end
end
While this solution would require twice as many rows in the join table it might be well worth the tradeoff as dealing with relations where the foreign keys are not fixed is a nightmare in ActiveRecord. This just works.
AR does not really let you provide the join sql when using .eager_load and .includes and loading records with a custom query and getting AR to make sense if the results and treat the associations as loaded to avoid n+1 query issues can be extremely hacky and time consuming. Sometimes you just have to build your schema around AR rather then trying to beat it into submission.
You would setup a synonym relationship between two words with:
happy = Word.create!(text: 'Happy')
jolly = Word.create!(text: 'Jolly')
# wrapping this in a single transaction is slightly faster then two transactions
Synonymity.transaction do
happy.synonyms << jolly
jolly.synonyms << happy
end
irb(main):019:0> happy.synonyms
Word Load (0.3ms) SELECT "words".* FROM "words" INNER JOIN "synonymities" ON "words"."id" = "synonymities"."synomym_id" WHERE "synonymities"."word_id" = $1 LIMIT $2 [["word_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Word id: 2, text: "Jolly", created_at: "2020-07-06 09:00:43", updated_at: "2020-07-06 09:00:43">]>
irb(main):020:0> jolly.synonyms
Word Load (0.3ms) SELECT "words".* FROM "words" INNER JOIN "synonymities" ON "words"."id" = "synonymities"."synomym_id" WHERE "synonymities"."word_id" = $1 LIMIT $2 [["word_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Word id: 1, text: "Happy", created_at: "2020-07-06 09:00:32", updated_at: "2020-07-06 09:00:32">]>
If you really want to setup associations where the record can be in either column on the join table you need one has_many association and one indirect association for each potential foreign key.
Bear with me here as this gets really crazy:
class Word < ActiveRecord::Base
has_many :synonym_pairs_as_word_1,
class_name: 'SynonymPair',
foreign_key: 'word_1'
has_many :synonym_pairs_as_word_2,
class_name: 'SynonymPair',
foreign_key: 'word_2'
has_many :word_1_synonyms,
through: :synonym_pairs_as_word_1,
class_name: 'Word',
source: :word_2
has_many :word_2_synonyms,
through: :synonym_pairs_as_word_2,
class_name: 'Word',
source: :word_1
def synonyms
self.class.where(id: word_1_synonyms).or(id: word_2_synonyms)
end
end
Since synonyms here still is not really an association you still have a potential n+1 query issue if you are loading a list of words and their synonyms.
While you can eager load word_1_synonyms and word_2_synonyms and combine them (by casting into arrays) this poses a problem if you need to order the records.
You are probably looking for the scope ActiveRecord class method:
class SynonymPair < ActiveRecord::Base
belongs_to :word1, class_name: :Word
belongs_to :word2, class_name: :Word
scope :with_word, -> (word) { where(word1: word).or(where(word2: word)) }
end
class Word < ActiveRecord::Base
scope :synonyms_for, -> (word) do
pairs = SynonymPair.with_word(word)
where(id: pairs.select(:word1_id)).where.not(id: word.id).or(
where(id: pairs.select(:word2_id)).where.not(id: word.id))
end
def synonyms
Word.synonyms_for(self)
end
end
I have two objects: Team and Player. As you could probably guess, a Team has many Players and a Player belongs to a team.
I understand that we can model this relationship with another model, Team_Players but I want to focus on the raw relationship here, as shown by many of the guides I'm seeing online.
When creating Players, it's easy to assign them a Team.id, since they only have one, but the reverse -has_many- is more complicated. The Rails guide on associations only shows the model file, so this is what mine looks like in reflection:
class Team < ActiveRecord::Base
has_many :players
end
Now, I would expect to be able to do something like Team.first.players and be given an array or something, but instead I just get undefined method player for #<Team:0x> and in fact in this video, I do see a developer doing something just like that. So what am I missing? Do I have to make an intersection model here? I would imagine not since has_many is inherent in Rails.
After creating the tables, I added the reference for Team to Player in this migration:
def change
add_reference :players, :team, index: true
add_foreign_key :players, :team
end
Again, since the has many relationship can't be modeled with a single column, I avoided that part in the migration. Is that what's necessary for the desired functionality of Team.first.players returning an array or something?
Here's what I did to get this to work:
rails new teams - followed by bundle
rails g model Team name:string player_count:integer
rails g model Player player_name:string player_number:integer
rails g migration add_team_id_to_players:
class AddTeamIdToPlayers < ActiveRecord::Migration
def change
add_column :players, :team_id, :integer
end
end
rake db:migrate
Here are my models:
class Player < ActiveRecord::Base
belongs_to :team
end
class Team < ActiveRecord::Base
has_many :players
end
Then, in the console:
Team.create(name: "Cats", player_count:1).save
Player.create(player_name: "Ryan", player_number:1, team_id:1).save
Then voila:
Team.first.players returns:
Team Load (0.2ms) SELECT "teams".* FROM "teams" ORDER BY "teams"."id" ASC LIMIT 1
Player Load (0.1ms) SELECT "players".* FROM "players" WHERE "players"."team_id" = ? [["team_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Player id: 2, player_name: "ryan", player_number: 1, created_at: "2015-12-18 19:32:39", updated_at: "2015-12-18 19:32:56", team_id: 1>]>
I have a many-to-many relation between Resumes and Educations which allows for the same education entry to appear on multiple resumes. When displaying Educations on a resume, I want the Educations to be ordered for that specific resume. To do this I set up a join table, Educations_Resumes, with the order information as a property.
However, when I try some like resume.educations I get the following error:
ActiveRecord::StatementInvalid: SQLite3::SQLException: near "order": syntax error:
SELECT "educations".* FROM "educations" INNER JOIN "educations_resumes" ON
"educations"."id" = "educations_resumes"."education_id" WHERE
"educations_resumes"."resume_id" = 2 ORDER BY educations_resumes.order
The models are setup as:
class Resume < ActiveRecord::Base
has_many :educations_resumes
has_many :educations, :through => :educations_resumes,
:order => 'educations_resumes.order'
end
class EducationsResume < ActiveRecord::Base
belongs_to :resume
belongs_to :education
end
class Education < ActiveRecord::Base
has_many :educations_resumes
has_many :resumes, :through => :educations_resumes
end
Any suggestions on how to correctly order resume.educations would be greatly appreciated
You are using a keyword as a column name:
http://www.sqlite.org/lang_keywords.html
Run a migration and change the column name:
rails g migration FixColumnName
This will give you a empty migration file that you should fill in with something like:
class FixColumnName < ActiveRecord::Migration
def change
rename_column :educations_resumes, :order, :ordering
end
end
Suppose you have the following models:
class Category < ActiveRecord::Base
has_one :current_heat, class_name: 'Heat'
has_many :scores, :through => :current_heat
end
class Heat < ActiveRecord::Base
belongs_to :category
has_many :scores
end
class Score < ActiveRecord::Base
belongs_to :heat
end
Surprisingly, when I invoke Category.first.scores ActiveRecord produces the following queries:
SELECT `categories`.* FROM `categories` LIMIT 1
SELECT * FROM `scores` INNER JOIN `heats` ON `scores`.`heat_id` = `heats`.`id` WHERE `heats`.`category_id` = 1
The above query ignores the has_one nature of Category#current_heat. I would have expected something more like:
SELECT `categories`.* FROM `categories` LIMIT 1
SELECT `heats`.* FROM `heats` WHERE `heats`.`category_id` = 1 LIMIT 1
SELECT * FROM `scores` WHERE `scores`.`heat_id` = 6
which is produced only when you explicitly traverse the has_one association from the root with Category.first.current_heat.scores.
It's as if ActiveRecord is silently treating my has_one as a has_many. Can someone explain this behavior to me? Is there an elegant workaround or a "right way" to do it?
Maybe you could remove the
has_many :scores, :through => :current_heat
and instead just delegate :scores through the has_one:
delegate :scores, :to => :current_heat
that would preserve your desired access method Category.first.scores.
has_one doesn't really exist to babysit your database in this fashion. It won't throw errors if there is more than one record that matches the foreign_key, it will just choose the first one. It assumes you haven't errantly added extra records which would break the has_one relation on your own.
In conclusion, the sql that it generates is fine as long as there is only one record attached to the Category. If somehow you've added extra records which shouldn't exist since it is a has_one, then it won't work, but it's not the job of activerecord to tell you that this has happened.
So I have two models: User and ScheduledSession
The users have roles. One of the roles is "instructor".
ScheduledSession
belongs_to :instructor, :class_name => 'User', :foreign_key => 'instructor_id'
User
has_many :scheduled_sessions
GOOD! So I can do this...
s = ScheduledSession.first
s.instructor
BAD! But I can't do this...
u = User.first
u.scheduled_sessions
I get this error...
SQLite3::SQLException: no such column: scheduled_sessions.user_id: SELECT "scheduled_sessions".* FROM "scheduled_sessions" WHERE "scheduled_sessions"."user_id" = 1
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: scheduled_sessions.user_id: SELECT "scheduled_sessions".* FROM "scheduled_sessions" WHERE "scheduled_sessions"."user_id" = 1
How can I setup the relationship the other way around so I can see which scheduled sessions belongs to that instructor (user)?
You just need to set the foreign key in the User has_many relationship as well.
has_many :scheduled_sessions, :foreign_key => 'instructor_id'