First of all, based on this (Rails association with multiple foreign keys) I figured out how to make two belong_to pointing to the same table.
I have something like that
class Book < ApplicationRecord
belongs_to :author, inverse_of: :books
belongs_to :co_author, inverse_of: :books, class_name: "Author"
end
class Author < ApplicationRecord
has_many :books, ->(author) {
unscope(:where).
where("books.author_id = :author_id OR books.co_author_id = :author_id", author_id: author.id)
}
end
It's all good. I can do either
book.author
book.co_author
author.books
However, sometimes I need to eager load books for multiple authors (to avoid N queries).
I am trying to do something like:
Author.includes(books: :title).where(name: ["Lewis Carroll", "George Orwell"])
Rails 5 throws at me: "ArgumentError: The association scope 'books' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported."
I am trying to figure out what I should do?
Should I go with many-to-many association? It sounds like a solution. However, it looks like it will introduce it's own problems (I need "ordering", meaning that I need explicitly differentiate between main author and co-author).
Just trying to figure out whether I am missing some simpler solution...
Why do you not use HABTM relation? For example:
# Author model
class Author < ApplicationRecord
has_and_belongs_to_many :books, join_table: :books_authors
end
# Book model
class Book < ApplicationRecord
has_and_belongs_to_many :authors, join_table: :books_authors
end
# Create books_authors table
class CreateBooksAuthorsTable < ActiveRecord::Migration
def change
create_table :books_authors do |t|
t.references :book, index: true, foreign_key: true
t.references :author, index: true, foreign_key: true
end
end
end
You can use eagerload like as following:
irb(main):007:0> Author.includes(:books).where(name: ["Lewis Carroll", "George Orwell"])
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."name" IN (?, ?) LIMIT ? [["name", "Lewis Correll"], ["name", "George Orwell"], ["LIMIT", 11]]
HABTM_Books Load (0.1ms) SELECT "books_authors".* FROM "books_authors" WHERE "books_authors"."author_id" IN (?, ?) [["author_id", 1], ["author_id", 2]]
Book Load (0.1ms) SELECT "books".* FROM "books" WHERE "books"."id" IN (?, ?) [["id", 1], ["id", 2]]
Try this:
Author.where(name: ["Lewis Carroll", "George Orwell"]).include(:books).select(:title)
Related
Why does EVERY single belongs_to association need to be selected first just to call update!? This seems a bit ridiculous, and I don't remember this being like that before, maybe I was using a deprecated method or something.
I know there is update_attribute which doesn't have this problem, but I want to update multiple attributes at once using a bang method.
I have some records with 5 associations, and just to update one or two columns it automatically does...
SELECT * FROM a
SELECT * FROM b
SELECT * FROM c
SELECT * FROM d
SELECT * FROM e
UPDATE f
I also am not using any validations at all, nor validates_associated
Model:
class Lead < ApplicationRecord
belongs_to :organization
belongs_to :vendor
belongs_to :prospect
belongs_to :visit
belongs_to :session
has_one :result
enum status: [
'PENDING',
'COMPLETE',
]
end
and
lead = Lead.first
lead.update!(status: 1)
[8] pry(main)> s.update!(slug: 'x')
TRANSACTION (0.2ms) BEGIN
Campaign Load (0.5ms) SELECT "campaigns".* FROM "campaigns" WHERE "campaigns"."id" = $1 LIMIT $2 [["id", "76dfe777-f563-43b1-900b-a2c40ea7d072"], ["LIMIT", 1]]
Sequence Update (0.6ms) UPDATE "sequences" SET "slug" = $1, "updated_at" = $2 WHERE "sequences"."id" = $3 [["slug", "x"], ["updated_at", "2023-01-27 01:49:32.472637"], ["id", "19c41977-71c6-4c60-a6b6-7af6ce090d80"]]
TRANSACTION (0.7ms) COMMIT => true
All columns look like this:
t.references :prospect, null: false, foreign_key: true, type: :uuid
belongs_to associations are required by default, which automatically adds presence validation.
:required
When set to true, the association will also have its
presence validated. This will validate the association itself, not the
id. You can use :inverse_of to avoid an extra query during validation.
NOTE: required is set to true by default and is deprecated. If you
don't want to have association presence validated, use optional: true.
https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to
I don't know what :inverse_of is supposed to do in this situation, doesn't seem to do anything.
Let's say you have this model:
class Post < ApplicationRecord
belongs_to :user
end
belongs_to adds a presence validator:
>> Post.validators
=> [#<ActiveRecord::Validations::PresenceValidator:0x00007f1fa2e96c40 #attributes=[:user], #options={:message=>:required}>]
In short, it does this to validate user: post.user.blank?, which loads the association.
You can set association as optional:
class Post < ApplicationRecord
belongs_to :user, optional: true
end
>> Post.validators
=> []
and add your own validations if you want:
class Post < ApplicationRecord
belongs_to :user, optional: true
validates :user, presence: true, on: :create
validates :user_id, presence: true, on: :update
end
Which only sort of works and breaks in some situations.
It will probably be best to handle it outside of the model with a custom validator and just skip model validations.
To update multiple attributes:
post = Post.first
post.assign_attributes(title: "name")
post.save!(validate: false)
There is also a config:
https://guides.rubyonrails.org/configuring.html#config-active-record-belongs-to-required-by-default
I created a table for items and a table for jobs. I then created a join table, Items Jobs.
This is my migration for the join table and the models:
class CreateItemsJobs < ActiveRecord::Migration[7.0]
def change
create_table :items_jobs, id: false do |t|
t.belongs_to :item
t.belongs_to :job
t.timestamps
end
end
end
class Item < ApplicationRecord
belongs_to :part
belongs_to :employee, optional: true
has_and_belongs_to_many :jobs
end
class Job < ApplicationRecord
belongs_to :client
belongs_to :employee
has_and_belongs_to_many :items
end
class ItemsJobs < ApplicationRecord
belongs_to :item
belongs_to :job
end
I then migrate successfully...
rails db:migrate ==>
== 20220210032352 CreateItemsJobs: migrating ==================================
-- create_table(:items_jobs, {:id=>false})
-> 0.0085s
== 20220210032352 CreateItemsJobs: migrated (0.0086s) =========================
But if I try to seed, I get an error. If I run my rails console, I can't add to the table until I attempt to view a table that doesn't exist.
rails c ==>
Loading development environment (Rails 7.0.1)
2.7.4 :001 > ItemsJobs.all
Traceback (most recent call last):
(irb):1:in `<main>': uninitialized constant ItemsJobs (NameError)
Did you mean? ItemJob
2.7.4 :002 > ItemJob.all
Traceback (most recent call last):
(irb):2:in `<main>': uninitialized constant ItemJob (NameError)
Did you mean? ItemsJobs
2.7.4 :003 > ItemsJobs.all
ItemsJobs Load (0.4ms) SELECT "items_jobs".* FROM "items_jobs"
=> []
2.7.4 :004 > ItemsJobs.create(item_id: 1, job_id: 1)
TRANSACTION (0.2ms) BEGIN1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
Job Load (0.3ms) SELECT "jobs".* FROM "jobs" WHERE "jobs"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
ItemsJobs Create (0.6ms) INSERT INTO "items_jobs" ("item_id", "job_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) [["item_id", 1], ["job_id", 1], ["created_at", "2022-02-10 15:23:44.127164"], ["updated_at", "2022-02-10 15:23:44.127164"]]
TRANSACTION (1.1ms) COMMIT
=>
#<ItemsJobs:0x00007f33c0aa7a80
item_id: 1,
job_id: 1,
created_at: Thu, 10 Feb 2022 15:23:44.127164000 UTC +00:00,
updated_at: Thu, 10 Feb 2022 15:23:44.127164000 UTC +00:00>
What is going wrong here? Why can't I view/add to the ItemsJobs table until I've attempted to view the suggested, non-existent ItemJob table?
You're mixing up has_and_belongs_to_many and has_many through: and the weird autoloading behavior is most likely because the naming scheme is throwing off how ActiveRecord maps classes to database tables through convention over configuration. The rule of thumb is that ActiveRecord expects models (class names) to be singular and tables to be plural.
HABTM assocations don't use a model for the join table. Rather its just a "headless" assocation where you just implicitly add/remove rows through the assocations on each end. HABTM is really only good for the case where you know that you won't need to query the table directly or access any additional columns - in other words it's quite useless outside of that niche. HABTM is the only place in ActiveRecord where you use the plural_plural naming scheme for tables - using that scheme with a model will cause constant lookup errors unless you explicitly configure everything.
If you want to setup an assocation with a join model (I would recommend this) you need to name the class and tables correctly and use an indirect assocation:
class CreateItemJobs < ActiveRecord::Migration[7.0]
def change
# table name should be singular_plural
create_table :item_jobs do |t|
t.belongs_to :item
t.belongs_to :job
t.timestamps
end
end
end
# model class names should always be singular
class ItemJob < ApplicationRecord
belongs_to :item
belongs_to :job
end
class Item < ApplicationRecord
belongs_to :part
belongs_to :employee, optional: true
has_many :item_jobs
has_many :jobs, through: :item_jobs
end
class Job < ApplicationRecord
belongs_to :client
belongs_to :employee
has_many :item_jobs
has_many :items, through: :item_jobs
end
Since you probally haven't run the migration in production and don't have any meaningful data in the table you can roll the bad CreateItemsJobs migration back and delete it. rails g model ItemJob item:belongs_to job:belongs_to will create both the model and migration.
See:
https://guides.rubyonrails.org/active_record_basics.html#convention-over-configuration-in-active-record
https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
I have a Selection model which has many Choices and one DefaultChoice.
The relation is structured this way.
Models (I think here there is something wrong)
class Selection < ApplicationRecord
has_many :choices, dependent: :delete_all
has_one :default_choice, class_name: 'Choice'
end
class Choice < ApplicationRecord
belongs_to Selection
end
Migration
create_table :selections do |t|
t.references :default_choice, index: true
end
create_table :choices do |t|
t.belongs_to :selection
end
Somehow something is not right:
# let's say:
selection = Selection.find(1)
selection.choices << Choice.find(1)
selection.choices << Choice.find(2)
selection.default_choice = Choice.find(2)
selection.save!
# then
selection.default_choice_id = 2
# but
selection.default_choice.id = 1
How come?!
selection.default_choice generates this query:
Choice Load (0.5ms) SELECT "choices".* FROM "choices" WHERE "choices"."selection_id" = $1 LIMIT $2 [["selection_id", 1], ["LIMIT", 1]]
You're using the same model Choice for both relationships has_many :choiches and has_one :default_choice. So when you query has_one :default_choice result are all from choices table and with has_one you only get one result which is the first it finds referenced to Selection object.
UPDATE
To implement default on has_many you can do something like add column on Choice model that will be true if it's default choice. Then has_one relationship would need to have scope to get only choice that is default true like this:
has_one :default_choice, -> { where default_choice: true }, class_name: 'Choice'
class Project < ActiveRecord::Base
has_many :accounts
has_many :sites, through: :accounts
end
class Site < ActiveRecord::Base
has_many :accounts
has_many :projects, through: :accounts
accepts_nested_attributes_for :accounts
end
class Account < ActiveRecord::Base
belongs_to :site
belongs_to :project
end
p = Project.find(1)
2.1.4 :011 > p.sites.create({"url"=>"site.ru", "accounts_attributes"=>{"0"=>{"email"=>"mail#site.ru"}}})
(0.3ms) BEGIN
SQL (1.8ms) INSERT INTO `sites` (`created_at`, `updated_at`, `url`) VALUES ('2015-09-04 07:09:53', '2015-09-04 07:09:53', 'site.ru')
SQL (0.3ms) INSERT INTO `accounts` (`created_at`, `email`, `site_id`, `updated_at`) VALUES ('2015-09-04 07:09:53', 'mail#site.ru', 3, '2015-09-04 07:09:53')
SQL (0.3ms) INSERT INTO `accounts` (`created_at`, `project_id`, `site_id`, `updated_at`) VALUES ('2015-09-04 07:09:53', 1, 3, '2015-09-04 07:09:53')
(1.2ms) COMMIT
=> #<Site id: 3, url: "site.ru", created_at: "2015-09-04 07:09:53", updated_at: "2015-09-04 07:09:53">
Question:
Why are added 2 record?
To add a single entry in the Account model with fields site_id, project_id, email?
The first account record is created automatically, because the Site is related to the Project through the Account.
The second record is created because you have accepts_nested_attributes_for :accounts in your Site model, and you pass the nested attributes while creating the Site record.
Could you clarify, what you want to archieve?
I have two tables: Group and Keyword. They have a many-to-many relationship via another table called Assignments. I have a rake task where I generate random keywords, save the keyword to the Keyword's table and assign it to the current group's keywords. The code looks like this (I used a similar syntax to what I use in the console; however, I am not sure about it's correctness):
Group.populate 30 do |group|
Faker::Lorem.words(rand(3..7)).each do |key|
k = Keyword.create(name: key)
group.keywords << k
end
end
The << is returning this error: NoMethodError: undefined method<<'`.
If I ran the inner two lines in the console I get what I expect (note: I simplified the below models so you will see more keys in the log, but ignore that):
(0.1ms) begin transaction
SQL (2.0ms) INSERT INTO "keywords" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "bla"], ["created_at", "2015-03-14 18:25:29.179793"], ["updated_at", "2015-03-14 18:25:29.179793"]]
(1.2ms) commit transaction
=> #<Keyword id: 1, name: "bla", created_at: "2015-03-14 18:25:29", updated_at: "2015-03-14 18:25:29">
(0.2ms) begin transaction
SQL (1.8ms) INSERT INTO "assignments" ("group_id", "keyword_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["group_id", 1], ["keyword_id", 1], ["created_at", "2015-03-14 18:26:53.650257"], ["updated_at", "2015-03-14 18:26:53.650257"]]
(0.8ms) commit transaction
=> #<ActiveRecord::Associations::CollectionProxy [#<Keyword id: 1, name: "bla", created_at: "2015-03-14 18:25:29", updated_at: "2015-03-14 18:25:29">]>
The Group model looks like this:
class Group < ActiveRecord::Base
has_many :assignments, dependent: :destroy
has_many :keywords, through: :assignments, dependent: :destroy
end
The Keyword model looks like that:
class Keyword < ActiveRecord::Base
has_many :assignments
has_many :groups, through: :assignments
end
And the Assignment model looks like this:
class Assignment < ActiveRecord::Base
belongs_to :group
belongs_to :keyword
end
How can I modify this code to work as expected?
TRy this
class Group < ActiveRecord::Base
has_many :assignments, dependent: :destroy
has_many :active_assignments, dependent: :destroy, class_name: "Assignment"
has_many :keywords, through: :active_assignments, source: :keyword
class Keyword < ActiveRecord::Base
has_many :assignments, dependent: :destroy
has_many :groups, through: :assignments
end
class Assignment < ActiveRecord::Base
belongs_to :group
belongs_to :keyword
end
Group.populate 30 do |group|
Faker::Lorem.words(rand(3..7)).each do |key|
group.keywords << Keyword.create(name: key)
end
end
I found the problem in my program. Even though it was a stupid mistake from me, I will leave the question and the error reported in case anybody fall into the same issue in the future.
I had this error because I had a field in my Group table which is named keywords, which of course conflicted with the Keywords table (which has many-to-many relation to Groups). I had this field from before I decided to create the many-to-many relation, and I forgot to remove it. Then, of course when you type something like this command group.keywords << Keyword.create(name: key) you will get this weird error: NoMethodError: undefined method<<.
However, fixing this mistake will not make the rake task run. This is because Group.populate returns a Populator object. If you run this task you will get this error: NoMethodError: undefined method keywords for #<Populator::Record:0x007fa1ee78fba8>. To fix that issue I had to assign the foreign keys manually in the task like so:
Group.populate 30 do |group|
Faker::Lorem.words(rand(3..7)).each do |key|
k = Keyword.create(name: key)
a = Assignment.create(keyword_id: k.id, group_id: group.id)
end
end
which will run as expected.