Get unexpected result from has_one relationship - ruby-on-rails

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'

Related

Why does ActiveRecord select every single association to update a single column?

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

How can I directly update the join model in a has_many_through association?

I have a Rails app with two models, joined by a third:
class Food < ApplicationRecord
has_many :shopping_list_items
has_many :users, through: :shopping_list_items
end
class ShoppingListItem < ApplicationRecord
belongs_to :user
belongs_to :food
end
class User < ApplicationRecord
has_many :shopping_list_items
has_many :foods, through: :shopping_list_items
end
The middle model, ShoppingListItem, has a few extra attributes, including priority which I'd like to update directly.
So for instance, I'd like to do something like:
r = current_user.shopping_list_items.where(food_id: 1).first
r.priority = "urgent"
r.save
The object looks fine until I try to save it, when I get a SQL error:
ActiveRecord::StatementInvalid (PG::SyntaxError: ERROR: zero-length delimited identifier at or near """"
LINE 1: ...$1, "updated_at" = $2 WHERE "shopping_list_items"."" IS NULL
^
: UPDATE "shopping_list_items" SET "priority" = $1, "updated_at" = $2 WHERE "shopping_list_items"."" IS NULL):
I guess it's complaining about the absence of a primary key? Not sure how to fix this, since the rails docs say that join tables shouldn't have a primary key column...
I created the middle table with a migration like this, :
create_table :shopping_list_items, id: false do |t|
t.belongs_to :user
t.belongs_to :food
t.string :priority
t.integer :position
t.timestamps
end

Rails/ActiveRecord: Can I establish polymorphic relationships with tables with differing id types?

Establishing a many to many polymorphic association is a real head twister. I've already spent the afternoon on this and I'm running out of energy.
I have two models, Comment and Project. I have a third model Badge. A user can react to a comment or project by adding a badge. Comments and projects will be reaction_targets.
I think I understand how to do the many to many polymorphic part. The trouble is Comment and Project have different id types. Comment is indexed by bigint whereas Project uses uuid.
My join table is called reactions through which Badge points to reaction_targets
class Badge < ApplicationRecord
has_many :reactions
has_many :reaction_targets, through: :reactions
end
Each row in the join table points to a badge and a target.
class CreateReactions < ActiveRecord::Migration[5.2]
def change
create_table :reactions do |t|
t.references :reaction_target, polymorphic: true
t.references :badge
end
end
end
I've established that the association with reaction_target is polymorphic.
class Reaction < ApplicationRecord
belongs_to :badge
belongs_to :reaction_target, polymorphic: true
end
I point Comment at the join table.
class Comment < ApplicationRecord
has_many :reactions, as: :reaction_target
has_many :badges, through: :reactions
end
Everything works correctly:
> b = Badge.first_or_create(name: 'badge')
> c = Comment.create
> c.badges
#=> []
> c.badges << b
#=> (0.2ms) SAVEPOINT active_record_1...
However things are different when it comes to Project.
I establish the association in the same manner:
class Project < ApplicationRecord
has_many :reactions
has_many :badges, through: :reactions
end
But it doesn't work the same.
> b = Badge.first_or_create(name: 'badge')
> p = Project.create
> p.badges
#=> #<Badge::ActiveRecord_Associations_CollectionProxy:0x3fdc34cde54c>
The first thing I see is that #badges returns an object other than an empty array.
And when I try to add a badge to the collection, I get an error:
(1.5ms) SAVEPOINT active_record_1
ActiveRecord::StatementInvalid: PG::InFailedSqlTransaction: ERROR: current transaction is aborted, commands ignored until end of transaction block
: SAVEPOINT active_record_1
from /Users/123/.rvm/gems/ruby-2.6.0/gems/activerecord-5.2.2/lib/active_record/connection_adapters/postgresql/database_statements.rb:75:in `async_exec'
Caused by PG::InFailedSqlTransaction: ERROR: current transaction is aborted, commands ignored until end of transaction block
from /Users/123/.rvm/gems/ruby-2.6.0/gems/activerecord-5.2.2/lib/active_record/connection_adapters/postgresql/database_statements.rb:75:in `async_exec'
I can see in schema.rb, reaction_target_id is expected to be a bigint.
create_table "reactions", force: :cascade do |t|
t.string "reaction_target_type"
t.bigint "reaction_target_id"
t.bigint "badge_id"
t.index ["badge_id"], name: "index_reactions_on_badge_id"
t.index ["reaction_target_type", "reaction_target_id"], name: "index_reactions_on_reaction_target_type_and_reaction_target_id"
end
I assume the error above is because of that. Is it possible to establish polymorphic relationships with tables that have differing index types? If so, what do I need to do to be able to add badges to projects? If not, is there a workaround?
You're missing as: :reaction_target on the has_many :reactions association in Project.
class Project < ApplicationRecord
has_many :reactions, as: :reaction_target
has_many :badges, through: :reactions
end
This is what specifies the polymorphic association which you declared as reaction_target.

Two belong_to referring the same table + eager loading

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)

belongs_to with foreign_key not working in one direction

class Position < ActiveRecord::Base
belongs_to :product, foreign_key: :symbol
end
class Product < ActiveRecord::Base
has_many :positions, primary_key: :symbol, foreign_key: :symbol
end
When I do
Product.first.positions.first I am getting a Product back.
But, when I do Position.first.product I am getting nothing.
When I look at the SQL generated by the query, it is:
SELECT "products.*" FROM "products" WHERE "products.id" = ? LIMIT 1 [["id", 0]]
Why?
The SQL generated is using products.id instead of products.symbol because you didn't tell it that the association should use symbol as the primary key instead of the default of id. So, in your Position class just add primary_key: :symbol to the belongs_to and I think that'll do it.
Try this:
class Product < ActiveRecord::Base
self.primary_key = "symbol"
end
First of all, you need to revise the creation of your model Product.
You need to create it the following way:
class CreateProducts < ActiveRecord::Migration
def change
create_table :products, id: false do |t|
t.string :symbol, null: false
t.timestamps
end
add_index :products, :symbol, unique: true
end
end
And then let your model know about the primary_key, that is not id:
class Product < ActiveRecord::Base
self.primary_key = "symbol"
end
And after that, when you do Product.last, it will generate the following query:
Product.last
# Product Load (0.3ms) SELECT "products".* FROM "products" ORDER BY "products"."symbol" DESC LIMIT 1

Resources