How to access other columns of has_many through table - ruby-on-rails

I am working on rails 7 with 2 models which are Partner and Package, and I would like to assign the packages to the partner, but they need to be assigned together with their orders.
For example, if I assign package#A and package#B to the partner with 1st and 2nd order respectively, then package#A will be considered as a small package and package#B will be considered as a medium package.
So, I have created PartnerPackage to link them together (with has_many through:) and also added package_order column to contain the package's order.
Question
What is the best way to retrieve all of the packages that belong to a specific partner, and also package_order as well? Thank you so much for your answer!
Partner.rb
class Partner < ApplicationRecord
has_many :partner_packages, dependent: :destroy
has_many :packages, through: :partner_packages
end
Package.rb
class Package < ApplicationRecord
has_many :partner_packages, dependent: :destroy
has_many :packages, through: :partner_packages
end
PartnerPackage.rb
class PartnerPackage < ApplicationRecord
belongs_to :partner
belongs_to :package
validates :package_order, presence: true
end

There's a virtually hidden feature of Rails that allows you to add any other columns to an SQL select and Rails makes them available as readonly attributes in the resulting object, eg:
[24] pry(main)> ff = Foo.select("*, current_timestamp as now").first
Foo Load (0.3ms) SELECT *, current_timestamp as now FROM "foos" ORDER BY "foos"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Foo:0x0000000106442580 id: 1, name: "First Foo", age: 1, created_at: Thu, 21 Jul 2022 00:13:26.758228000 UTC +00:00, updated_at: Thu, 11 Aug 2022 15:23:14.015000000 UTC +00:00>
[25] pry(main)> ff.now
=> "2022-08-18 16:40:25"
So then you can use this to pull out the fields in a hmt table because you know that the SQL will include a join on that table:
partner.packages.select("packages.*, partner_packages.package_order")
Now if you want to get fancy you can use Arel to avoid the string table names, but that's the basic idea.

Related

Converting SQL query into Custom Relations Query in Rails

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

What is causing this error with my Active Record associations when I use model.collection.build?

In my project, I have the following three classes:
class User < ApplicationRecord
has_many :portfolios, dependent: :destroy
has_many :positions, through: :portfolios
end
class Portfolio < ApplicationRecord
belongs_to :user
has_many :positions, dependent: :destroy
end
class Position < ApplicationRecord
belongs_to :portfolio
end
When I try to build a position directly off the user model (user.positions.build(attributes)) by passing in an existing portfolio_id as one of the attributes, I get the following error:
ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection (Cannot modify association 'User#positions' because the source reflection class 'Position' is associated to 'Portfolio' via :has_many.
Why would this happen? I feel there's something to be learned here but I don't really get it!
Addendum: I think my associations make sense: a portfolio should only belong to one user, a position to only one portfolio, and a portfolio should have multiple positions and a user multiple portfolios.
You need to build positions like this
user = User.first
portfolio_attributes = {name: 'Portfolio_1'}
position_attributes = {name: 'Postion_1'}
user.portfolios.build(portfolio_attributes).positions.build(position_attributes)
user.save!
When i run user.positions i get the below result
user.positions
Position Load (0.6ms) SELECT "positions".* FROM "positions" INNER JOIN "portfolios" ON "positions"."portfolio_id" = "portfolios"."id" WHERE "portfolios"."user_id" = ? LIMIT ? [["user_id", nil], ["LIMIT", nil]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Position id: 1, name: "Postion_1", portfolio_id: 1>]>

Rails has_many undefined method for association

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>]>

Multiple Associations in a Model

I have a User model and an Account model. The user has many accounts and the accounts belong to one user. I have the models and associations all set up. Now I want to make one of those accounts the "primary account". What is the best way to set up the associations? I added a primary_account_id column to my user table and set up the associations like this but it didn't work. Any tips?
class User < ActiveRecord::Base
has_many :accounts
has_one :primary_account, :class_name => "Account"
end
class Account < ActiveRecord::Base
belongs_to :user
end
Edit
I see this question Rails model that has both 'has_one' and 'has_many' but with some contraints which is very similar and the second answer makes the suggestion that I tried. However when I use it rails ignores the column that I've made and just grabs the first one in the table:
>> u = User.find(1)
User Load (3.9ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
=> #<User id: 1, email: "XXXXXXX#gmail.com", created_at: "2012-03-15 22:34:39", updated_at: "2012-03-15 22:34:39", primary_account_id: nil>
>> u.primary_account
Account Load (0.1ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."user_id" = 1 LIMIT 1
=> #<Account id: 5, name: "XXXXXX", created_at: "2012-03-16 04:08:33", updated_at: "2012-03-16 17:57:53", user_id: 1>
>>
So I created a simple ERD and your issue is very simple, but I think I found a serious issue:
class User < ActiveRecord::Base
has_many :accounts
has_one :primary_account, :class_name => "Account", :primary_key => "account_pimary_id"
end
class Account < ActiveRecord::Base
belongs_to :user
end
To get the associations as is, just set the :primary_key on has_one :primary_account so that it uses users.account_primary_id instead of users.id.
While this works, it will proboably cause nothing but problems. If Account's user_id is used as the foreign key for id and account_primary_id, you have no idea if an Account is a normal Account or a Primary Account without explicitly joining both id and account_primary_id every time. A foreign_key should only point at 1 column, in this case, User's table id. Then it is a straight shot into the Account's table.
#Zabba solution is the smart one, but just needs the :include for the join
has_one :primary_account, :class_name => "Account", :conditions => "users.primary_account_id = accounts.id", :include => :user
This means all Accounts belong to a User and only 1 is flagged as a primary account. Nice and straight forward, avoiding the wacky where clauses.

Trouble with rails naming conventions

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

Resources