I was unable to setup my tables using the id field as the primary_id (I mean they are there but they don't mean anything to the business). ie Both the sales and customer tables come from our Microsoft SQL database.
In the Sales table I have a field called 'customer_id' that matches the 'customer_id' (not the 'id' field) in the customers table. My 'customer_id' field datatype is set to integer but in the migrations I never specified whether they were keys. I only indexed them.
The sale belongs to the customer and the customer has many sales so I am trying to make the has_many and belongs_to association.
sale.rb
class Sale < ActiveRecord::Base
belongs_to :customer, primary_key: "customer_id", foreign_key: "customer_id"
end
customer.rb
class Customer < ActiveRecord::Base
has_many :sales
end
However when I get the rails console and try to test this I am unable to have the SQL search for the customer_id, only the id from the customers table is searched.
rails console
2.0.0-p353 :001 > c = Customer.find_by_customer_id(400123)
Customer Load (2.0ms) SELECT "customers".* FROM "customers"
WHERE "customers"."customer_id" = 400123 LIMIT 1
=> #<Customer id: 16271, customer_id: 400123, customer_name: "ABC",
customer_street: "ABC", customer_suburb: "ABC", customer_state: "ABC",
customer_postcode: "ABC", dispatch_location_price_list: "ABC", customer_selling_price_list:
"ABC", customer_status: "ABC", line_of_business_id: "ABC", line_of_business: "ABC",
market_segment_id: "ABC", market_segment_name: "Other", customer_rep_id: 123, customer_rep_name: "ABC"
2.0.0-p353 :002 > c.sales
Sale Load (7.1ms) **SELECT "sales".* FROM "sales"
WHERE "sales"."customer_id" = $1 [["customer_id", 16271]]**
=> #<ActiveRecord::Associations::CollectionProxy []>
Specifically I just want the last bit to say [["customer_id', 400123]].
I know it is probably obvious but as I am still learning a lot of this stuff takes a bit to sink in (alot of cases I have answered my question just by asking it but not so tonight). Any suggestions would be greatly appreciated.
In my research I have found this in the API:
4.2.2.7 :primary_key
By convention, Rails assumes that the column used to hold the primary
key of this model is id. You can override this and explicitly specify
the primary key with the :primary_key option.
Which made me think I should be using the primary_key as the 'id' field isn't the field to be looked up using.
I also read this thread but I think it may be a little different than my situation.
Have you tried to add self.primary_key = :customer_id to the Customer model?
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 three models: User, Company, and Subscription. What I am trying to accomplish is a Subscription belongs to either a User OR a Company.
To try accomplish this, I referenced this guide, but I have been unsuccessful as the record creation keeps rolling back.
here's my Company model:
# app/models/company.rb
class Company < ApplicationRecord
has_many :subscriptions, dependent: :destroy, as: :imageable
end
here's my User model:
# app/models/user.rb
class User < ApplicationRecord
has_many :subscriptions, dependent: :destroy, as: :imageable
end
and finally, here's my Subscription model:
class Subscription < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
Now as far as the migration file, this is my Subscription migration file:
class CreateSubscriptions < ActiveRecord::Migration[5.1]
def change
create_table :subscriptions do |t|
t.references :imageable, polymorphic: true, index: true
t.date :start_date
t.date :stop_date
t.timestamps
end
end
end
As far as what I can see, this is pretty much exactly like the guide shows, but it keeps rolling back. Here's the output of the rails console:
Loading development environment (Rails 5.1.6)
2.5.1 :001 > Subscription.create(imageable_id: 1, start_date: Time.now, stop_date: 2.days.from_now)
(8.6ms) SET NAMES utf8, ##SESSION.sql_mode = CONCAT(CONCAT(##sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), ##SESSION.sql_auto_is_null = 0, ##SESSION.wait_timeout = 2147483
(0.2ms) BEGIN
(0.3ms) ROLLBACK
=> #<Subscription id: nil, imageable_type: nil, imageable_id: 1, start_date: "2018-10-10", stop_date: "2018-10-12", created_at: nil, updated_at: nil>
2.5.1 :002 >
Here are the questions that I have:
Why is there an imageable_type field? Is that created by t.references and if so, do I need this? Can I just use imageable_id instead of t.references as the other part of the suggestion shows?
Why is it rolling back? Are polymorphic associations done differently in Rails 5.x or something by chance?
According to the graph shown in the guide, it looks like if a picture belongs to imageable_id 4, then if there is an employee AND a production with the ID of 4, then a picture would belongs to both instead of one or the other like I'm trying to accomplish. Correct?
In your association, Imageable type will contain the class name and imageble id will contain the id of that class. So if you want to create subscription for user you can do like below
User.first.subcriptions.create(start_date: Time.now, stop_date: 2.days.from_now)
So it will automatically pick up First user's id in imageable id and take "User" as imageable type.
If you want to create subscription manually, you must have to pass both fields imageable type and imageble id like below,
Subscription.create(imageable_id: 1, imageable_type: "User", start_date: Time.now, stop_date: 2.days.from_now)
Why is there an imageable_type field? Is that created by
t.references and if so, do I need this? Can I just use imageable_id
instead of t.references as the other part of the suggestion shows?
=> imageable_type will contain the class of associate model like "User" or "Company"
Why is it rolling back? Are polymorphic associations done
differently in Rails 5.x or something by chance?
=> No, you setup it correctly
According to the graph shown in the guide, it looks like if a picture belongs to imageable_id 4, then if there is an employee AND a production with the ID of 4, then a picture would belongs to both instead of one or the other like I'm trying to accomplish. Correct ?
=> It depends on both imageable_id and imageble_type , so by combination of both this you will get record. If imageable_id is 4 and imageable_type is "Picture" then it will take Picture with id 4.
Please check this link for understaing
For polymorphic association, you should also pass imageable_type along with imageable_id. You don't do it and that's why it doesn't work, most probably (i.e. there might be other reasons, I don't know, but this one is pretty obvious).
imageable_type holds the name of the class of the record given Subscription is associated to.
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 the following model:
class Section < ActiveRecord::Base
belongs_to :page
has_many :revisions, :class_name => 'SectionRevision', :foreign_key => 'section_id'
has_many :references
has_many :revisions, :class_name => 'SectionRevision',
:foreign_key => 'section_id'
delegate :position, to: :current_revision
def current_revision
self.revisions.order('created_at DESC').first
end
end
Where current_revision is the most recently created revision. Is it possible to turn current_revision into an association so I can perform query like Section.where("current_revision.parent_section_id = '1'")? Or should I add a current_revision column to my database instead of trying to create it virtually or through associations?
To get the last on a has_many, you would want to do something similar to #jvnill, except add a scope with an ordering to the association:
has_one :current_revision, -> { order created_at: :desc },
class_name: 'SectionRevision', foreign_key: :section_id
This will ensure you get the most recent revision from the database.
You can change it to an association but normally, ordering for has_one or belongs_to association are always interpreted wrongly when used on queries. In your question, when you turn that into an association, that would be
has_one :current_revision, class_name: 'SectionRevision', foreign_key: :section_id, order: 'created_at DESC'
The problem with this is that when you try to combine this with other queries, it will normally give you the wrong record.
>> record.current_revision
# gives you the last revision
>> record.joins(:current_revision).where(section_revisions: { id: 1 })
# searches for the revision where the id is 1 ordered by created_at DESC
So I suggest you to add a current_revision_id instead.
As #jvnill mentions, solutions using order stop working when making bigger queries, because order's scope is the full query and not just the association.
The solution here requires accurate SQL:
has_one :current_revision, -> { where("NOT EXISTS (select 1 from section_revisions sr where sr.id > section_revisions.id and sr.section_id = section_revisions.section_id LIMIT 1)") }, class_name: 'SectionRevision', foreign_key: :section_id
I understand you want to get the sections where the last revision of each section has a parent_section_id = 1;
I have a similar situation, first, this is the SQL (please think the categories as sections for you, posts as revisions and user_id as parent_section_id -sorry if I don't move the code to your need but I have to go):
SELECT categories.*, MAX(posts.id) as M
FROM `categories`
INNER JOIN `posts`
ON `posts`.`category_id` = `categories`.`id`
WHERE `posts`.`user_id` = 1
GROUP BY posts.user_id
having M = (select id from posts where category_id=categories.id order by id desc limit 1)
And this is the query in Rails:
Category.select("categories.*, MAX(posts.id) as M").joins(:posts).where(:posts => {:user_id => 1}).group("posts.user_id").having("M = (select id from posts where category_id=categories.id order by id desc limit 1)")
This works, it is ugly, I think the best way is to "cut" the query, but if you have too many sections that would be a problem while looping trough them; you can also place this query into a static method, and also, your first idea, have a revision_id inside of your sections table will help to optimize the query, but will drop normalization (sometimes it is needed), and you will have to be updating this field when a new revision is created for that section (so if you are going to be making a lot of revisions in a huge database it maybe would be a bad idea if you have a slow server...)
UPDATE
I'm back hehe, I was making some tests, and check this out:
def last_revision
revisions.last
end
def self.last_sections_for(parent_section_id)
ids = Section.includes(:revisions).collect{ |c| c.last_revision.id rescue nil }.delete_if {|x| x == nil}
Section.select("sections.*, MAX(revisions.id) as M")
.joins(:revisions)
.where(:revisions => {:parent_section_id => parent_section_id})
.group("revisions.parent_section_id")
.having("M IN (?)", ids)
end
I made this query and worked with my tables (hope I named well the params, it is the same Rails query from before but I change the query in the having for optimization); watch out the group; the includes makes it optimal in large datasets, and sorry I couldn't find a way to make a relation with has_one, but I would go with this, but also reconsider the field that you mention at the beginning.
If your database supports DISTINCT ON
class Section < ApplicationRecord
has_one :current_revision, -> { merge(SectionRevision.latest_by_section) }, class_name: "SectionRevision", inverse_of: :section
end
class SectionRevision < ApplicationRecord
belongs_to: :section
scope :latest_by_section, -> do
query = arel_table
.project(Arel.star)
.distinct_on(arel_table[:section_id])
.order(arel_table[:section_id].asc, arel_table[:created_at].desc)
revisions = Arel::Nodes::TableAlias.new(
Arel.sql(format("(%s)", query.to_sql)), arel_table.name
)
from(revisions)
end
end
It works with preloading
Section.includes(:current_revision)
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.