Delete has_one through: association - ruby-on-rails

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

Related

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 6: Can't delete nested model. Random Insert statement

I using Rails 6 with Postgres and having issues deleting a nested model.
A random insert statement gets generated after the association has been deleted.
Let me explain my set up.
Migrations
class CreateEntries < ActiveRecord::Migration[6.0]
def change
create_table :entries do |t|
t.string :name
t.timestamps
end
end
end
class Cards < ActiveRecord::Migration[6.0]
def change
create_table :cards do |t|
t.string :card_number
t.belongs_to :entry, null: true, foreign_key: true
t.timestamps
end
end
end
Models
class Entry < ApplicationRecord
has_one :card, dependent: :destroy
accepts_nested_attributes_for :card, allow_destroy: true
end
class Card < ApplicationRecord
belongs_to :entry
end
Controller
class EntriesController < ApplicationController
before_action :set_entry
def update
#entry.update(entry_params)
end
def set_entry
#entry = Entry.find(params[:id])
end
def entry_params
params.require(:entry).permit(:name,
card_attributes: [:id, :card_number, :_destroy]
)
end
end
Request Params
Parameters: {"authenticity_token"=>"CQ...Ucw==", "entry"=>{"card_attributes"=>{"_destroy"=>"true"}}, "id"=>"1"}
These are the logs
(0.2ms) BEGIN
ConcessionCard Load (0.2ms) SELECT "cards".* FROM "cards" WHERE "cards"."entry_id" = $1 LIMIT $2 [["entry_id", 1], ["LIMIT", 1]]
Card Destroy (0.4ms) DELETE FROM "cards" WHERE "cards"."id" = $1 [["id", 2]]
Card Create (0.6ms) INSERT INTO "cards" ("entry_id", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["entry_id", 1], ["created_at", "2019-09-06 13:50:41.100718"], ["updated_at", "2019-09-06 13:50:41.100718"]]
(0.3ms) COMMIT
Why is insert being generated after the delete call? It's not even a rollback.
Note: I have tried both null:true and null:false in the Cards belongs_to migration. I also tried setting optional:true in the belongs_to :entry statement in the Card model
Unless you include an id in card_attributes then Rails sees this as a new record, so it just replaces the has_one with a newly created Card for you (which because of your dependent: :destroy option deletes the existing associated Card).
Best to use a form.fields_for :card block in your form partial/view, which will automatically add the hidden id tag for an existing Card.

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.

Rails nested attributes and changing associations

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.

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