Rails nested attributes and changing associations - ruby-on-rails

This one has had me stumped all day!
I have the following models:
Pump class
class Pump < ApplicationRecord
has_one :control, as: :equipment
accepts_nested_attributes_for :control
Pump Schema
class CreatePumps < ActiveRecord::Migration[5.1]
def change
create_table :pumps do |t|
t.references :property, foreign_key: true, null: false
t.string :name, default: 'Pump', null: false
t.timestamps
end
end
end
Control class
class Control < ApplicationRecord
belongs_to :equipment, polymorphic: true
Control Schema
class CreateControls < ActiveRecord::Migration[5.1]
def change
create_table :controls do |t|
t.belongs_to :device, foreign_key: true, index: true
t.integer :position, index: true
t.references :equipment, polymorphic: true, index: true
t.belongs_to :control_type, foreign_key: true, index: true
t.timestamps
end
end
end
I'm trying to update the association between a Control and a Pump. The following works:
[439] pry(main)> Pump.first.update!(control: Control.find(62))
.
.
.
=> true
But the following doesn't and I can't figure out why.
[438] pry(main)> Pump.first.update(control_attributes: {id: 62})
(0.4ms) BEGIN
(0.4ms) ROLLBACK
ActiveRecord::RecordNotFound: Couldn't find Control with ID=62 for Pump
with ID=1
from /usr/local/bundle/gems/activerecord-
5.1.5/lib/active_record/nested_attributes.rb:584:in
`raise_nested_attributes_record_not_found!'
The context is that I have a form for a Pump and when editing my Pump, there's a list of Controls in a select drop-down. I would just like to choose which Control is associated with the pump.
Update1: Answering a question from below
[468] pry(main)> Pump.first.update(control_attributes: {id: 62})
Pump Load (1.0ms) SELECT "pumps".* FROM "pumps" ORDER BY "pumps"."id" ASC LIMIT $1 [["LIMIT", 1]]
(0.3ms) BEGIN
Control Load (0.4ms) SELECT "controls".* FROM "controls" WHERE "controls"."equipment_id" = $1 AND "controls"."equipment_type" = $2 LIMIT $3 [["equipment_id", 1], ["equipment_type", "Pump"], ["LIMIT", 1]]
(0.3ms) ROLLBACK
ActiveRecord::RecordNotFound: Couldn't find Control with ID=62 for Pump with ID=1
from /usr/local/bundle/gems/activerecord-5.1.5/lib/active_record/nested_attributes.rb:584:in `raise_nested_attributes_record_not_found!'

Pump.first.update(control_attributes: {id: 62})
Rails's nested attributes does't work this way! The code above means:
Find a control which's id is 62, and it's equipment_type should be "Pump", and it's equipment_id should be Pump.first.id, then update with extra params, which you did not provided.
You got this error because in the first step, the control with id 62, it's equipment_id isn't Pump.first.id
Like, to update name of the control which's id is 60, belongs to Pump.first, in correct association:
Pump.first.update(control_attributes: {id: 60, name: "xxxx"})

When you use accepts_nested_attributes_for for linked model, it will create new records when attributes are provided without id parameter. And it will update existing record linked with the parent record, when attributes are provided with id parameter.
ActiveRecord::RecordNotFound: Couldn't find Control with ID=62 for Pump
with ID=1 : This error states that there is no Control record found for Pump object with the mentioned Ids.
You can add new control record for pump as:
Pump.first.update(control_attributes: { attribute1: 'attribute1_value' } )
This will create a new Control record associated with the Pump object having Id 1. And now you can update this again as follows:
Pump.first.update(control_attributes: { id: 1, attribute1: 'updated_attribute1_value' } )
Note that id of the newly created Control record is taken as 1.
Please read through the documentation to get more details.
Hope this helps !

You could override the nested attributes setter method in the model so that it also updates the foreign key column directly.
# pump.rb
def control_attributes=(attributes)
if (new_control = Control.find_by(id: attributes[:id]))
self.control_id = new_control.id
end
super
end
Note: be careful about assigning the relation directly (i.e self.control = new_control) because that could result in some unexpected side effects if it is a has_one association defined with a :dependent option that results in deleting the record.

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

Join table has two plural words. How to properly name models and migrations; in order to destroy orphans on join-table. Active Record

Okay, setting aside the fact that asking how to "destroy orphans" on a chat board is just evil, here is my technical question:
I am making a simple blog-site, using Activerecord and Sinatra (and I am new to both). I have a "many to many" relationship between a Post model and a Tag model, and have been getting errors that I was not able to sort out. Finally, I arranged the models and migration so that I am not getting errors; however, when I destroy an instance of a tag, the the associations on the "posts_tags" table are left in place (existing as orphans), and I want to know how to destroy them (cue evil music).
Can anyone assist me with this?
Here are my migration for all three tables:
class CreatePostsTable < ActiveRecord::Migration[5.2]
def change
create_table :posts do |t|
t.string :title, default: 'title here'
t.string :content, default: 'content here'
t.belongs_to :blog, index: true
t.timestamps
end
end
end
class CreateTagTable < ActiveRecord::Migration[5.2]
def change
create_table :tags do |t|
t.string :name, unique: true
end
end
end
class CreatePostsTagsTable < ActiveRecord::Migration[5.2]
def change
create_table :posts_tags, :id => false do |t|
t.belongs_to :tag, index: true
t.belongs_to :post, index: true
# t.integer :post_id
# t.integer :tag_id
end
end
end
And here are my three models:
file-name: Post_Tag (I have named, and re-named this class and file-name over the course of a couple days).
class PostsTag < ActiveRecord::Base
# self.table_name = "posts_tags" #had to add this line for seed file not to give errors
belongs_to :post
belongs_to :tag
end
class Post < ActiveRecord::Base
belongs_to :blog
has_many :posts_tags
has_many :tags, :through => :posts_tags
has_many :comments, dependent: :destroy
end
class Tag < ActiveRecord::Base
has_many :posts_tags
has_many :posts, :through => :posts_tags
# has_and_belongs_to_many :posts, :through => :posts_tags
end
This configuration is not giving me error when I search for a post's tags, or a tag's posts, BUT when I do #tag = Tag.find_by_id(1) #tag.destroy the "posts_tags" table is left still having all of the tag_id == 1 rows still there. When I try to destroy them, I get errors (which I think is because that would also destroy the associations).
Does anyone know how to fix this? Is it okay to simply remove the PostsTag model all together?
The Activerecord documentation uses an example where the join-table is a single word, so I wasn't able to find the answer there.
I saw that some people were simply not making a model for the join-table. When I removed my join-table model, I got this error:
#post = Post.find_by_id(1)
#post.tags.each do |tag|
p tag.name
end
NameError at /
uninitialized constant Tag::PostsTag
Thanks for any help!
#
Phase Two of question:
After the tip that I could try adding this code to my models:
class Post < ActiveRecord::Base
has_many :posts_tags, dependent: :destroy
class Tag < ActiveRecord::Base
I am running this in my server/app file:
delete "/tag/destroy" do
body = JSON.parse request.body.read # body: {id: number} or {name: string}
#tag_to_destroy = Tag.where(body)[0]
#tag_to_destroy.destroy
redirect "/tag"
end
I am sending this request via postman:
http://localhost:4567/tag/destroy
body of request:
{
"id": 12
}
And I am getting this error:
(even though there is are rows in the posts_tags database with tag_id = 12, post_id = variousNumbers):
== Sinatra (v2.0.1) has taken the stage on 4567 for development with backup from Puma
Puma starting in single mode...
* Version 3.11.4 (ruby 2.5.1-p57), codename: Love Song
* Min threads: 0, max threads: 16
* Environment: development* Listening on tcp://localhost:4567
Use Ctrl-C to stop
D, [2018-05-07T10:54:19.604906 #99099] DEBUG -- : Tag Load (0.5ms) SELECT "tags".* FROM "tags" WHERE "t
ags"."id" = $1 [["id", 12]]
D, [2018-05-07T10:54:19.617955 #99099] DEBUG -- : (0.3ms) BEGIN
D, [2018-05-07T10:54:19.633736 #99099] DEBUG -- : PostsTag Load (1.5ms) SELECT "posts_tags".* FROM "pos
ts_tags" WHERE "posts_tags"."tag_id" = $1 [["tag_id", 12]]
D, [2018-05-07T10:54:19.642305 #99099] DEBUG -- : PostsTag Destroy (1.6ms) DELETE FROM "posts_tags" WHERE "posts_tags"."" IS NULL
D, [2018-05-07T10:54:19.642685 #99099] DEBUG -- : (0.2ms) ROLLBACK
2018-05-07 10:54:19 - ActiveRecord::StatementInvalid - PG::SyntaxError: ERROR: zero-length delimited identifier at or near """"
LINE 1: DELETE FROM "posts_tags" WHERE "posts_tags"."" IS NULL
^
: DELETE FROM "posts_tags" WHERE "posts_tags"."" IS NULL:
/Users/maiya/.rvm/gems/ruby-2.5.1/gems/activerecord-5.2.0/lib/active_record/connection_adapters/postgresql_adapter.rb:603:in `async_exec'
Question update:
The join-table migration was previously:
create_table :posts_tags :id => false do |t|
t.belongs_to :tag, index: true
# comment: the above line creates: t.integer :post_id
t.belongs_to :post, index: true
# comment: the above line creates: t.integer :tag_id
end
All is OK now with your migrations and models, you just need to change in the Post and Tag models:
has_many :posts_tags
to
has_many :posts_tags, dependent: :destroy
In this case after you destroy any tag or post all associated posts_tags are destroyed too, it will ensure referential integrity of your app and prevent errors.
Also, it is good idea to rollback migrations, add foreign_key: true option to t.belongs_to lines, and migrate it again. This option will provide referential integrity on db level.
UPDATE:
has_and_belongs_to_many (HABTM) doesn't work with through option, you mixes it up with has_many association. Please, read about it more in guides
To make it all working you need to:
remove :id => false option from CreatePostsTagsTable migration
remigrate or drop and recreate db
change has_and_belongs_to_many <...>, :through => :posts_tags to has_many <...>, :through => :posts_tags in both Tag and Post models
P.S. By convention all file names in ruby are in lower snake case, it should be `posts_tag.rb', not 'Posts_Tag.rb'

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)

Delete has_one through: association

I have
class Job < ApplicationRecord
has_one :user, through: :jobs_user
has_one :jobs_user, dependent: :destroy
end
and the model for the join_table looks like this:
class JobsUser < ApplicationRecord
belongs_to :job
belongs_to :user
end
The migration was:
create_join_table :jobs, :shops do |t|
t.index :job_id
end
When I create a job and try to delete it fails :
j = Job.create(user: User.last)
j.destroy!
Job Load (0.3ms) SELECT "jobs".* FROM "jobs" ORDER BY "jobs"."id" DESC LIMIT 1
(0.2ms) BEGIN
JobsShop Load (0.3ms) SELECT "jobs_shops".* FROM "jobs_shops" WHERE "jobs_shops"."job_id" = 21365 LIMIT 1 [["job_id", 21365]]
SQL (0.7ms) DELETE FROM "jobs_shops" WHERE "jobs_shops"."" = NULL [[nil, nil]]
(0.2ms) ROLLBACK
ActiveRecord::StatementInvalid: PG::SyntaxError: ERROR: zero-length delimited identifier at or near """"
LINE 1: DELETE FROM "jobs_shops" WHERE "jobs_shops"."" = NULL
^
: DELETE FROM "jobs_shops" WHERE "jobs_shops"."" = NULL
It seems I failed somewhere and it cannot find the column to destroy.
The answer can be found here : https://github.com/rails/rails/issues/25347#issuecomment-300067025
Active Record doesn't have built in support for composite primary keys
That means you can't manipulate a model whose corresponding table doesn't have a single-column primary key defined. That includes doing so through an association that uses said model.
So in my case, choosing create_join_table was not the right choice. Instead create a normal table.
create_table :users_jobs do |t|
t.integer :user_id
t.integer :job_id
# t.index :job_id
end

Rails belongs_to class_name with the correct foreign keys?

I have a fixtures and a teams database to show football fixtures.
I can currently view all fixtures(index & show..) and all teams(index only).
create_table "fixtures", :force => true do |t|
t.integer "home_id"
t.integer "away_id"
t.date "date"
...
create_table "teams", :force => true do |t|
t.string "name"
...
My Fixture & Team models are below;
class Fixture < ActiveRecord::Base
belongs_to :home, :class_name => 'Team'
belongs_to :away, :class_name => 'Team'
...
class Team < ActiveRecord::Base
has_many :fixtures
...
I get the following error when trying to display a teams fixtures on 'teams#show' page
Processing by TeamsController#show as HTML
Parameters: {"id"=>"3"}
Team Load (0.4ms) SELECT "teams".* FROM "teams" WHERE ("teams"."id" = 3) LIMIT 1
Fixture Load (0.3ms) SELECT "fixtures".* FROM "fixtures"
Fixture Load (0.3ms) SELECT "fixtures".* FROM "fixtures" WHERE ("fixtures".team_id = 3)
SQLite3::SQLException: no such column: fixtures.team_id: SELECT "fixtures".* FROM "fixtures" WHERE ("fixtures".team_id = 3)
I understand the error is saying it's looking for team_id in the fixtures but that doesn't exist as I have a home_id and away_id as each fixture always contains two teams, so how do I build a find to display an individual teams fixtures?
I wish there was a good way of doing this too, but there really isn't. Probably the best workarounds are:
To have two has_manys, :home_fixtures and :away_fixtures, and then a method fixtures that returns the union of the two (see this question for more details).
To skip out on the has_manys altogether and just write a method fixtures that gives you what you want without actually having and ActiveRecord relationship set up.
Like so (Rails 2.3-Style):
def fixtures
return Fixture.all(:conditions => ['away_id = :id OR home_id = :id', {:id => self.id}])
end
Hope this helps!

Resources