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.
Related
In an effort to create a Short, Self Contained, Correct (Compilable), Example, imagine that I want to do the following.
I have a blog website. There are two types of posts, TextPost and LinkPost. There are also two types of users, User and Guest. I would like to implement Multiple Table Inheritance with TextPost and LinkPost, by which I mean (hopefully I'm using the term correctly):
At the model level, I will have Post, TextPost and LinkPost. TextPost and LinkPost will inherit from Post.
At the database level, I will have tables for the "leaf" models of TextPost and LinkPost, but not for Post.
Each type of Post can belong to either a User or a Guest. So we have a polymorphic belongs_to situation.
My question is how to accomplish these goals.
I tried the following, but it doesn't work.
class Post < ApplicationRecord
self.abstract_class = true
belongs_to :author, polymorphic: true # user or guest
validates :title, :author_id, :author_type, presence: true
end
class TextPost < Post
validates :content, presence: :true
end
class LinkPost < Post
validates :url, presence: :true
end
class User < ApplicationRecord
has_many :text_posts, as: :author
has_many :link_posts, as: :author
validates :name, presence: true
end
class Guest < ApplicationRecord
has_many :text_posts, as: :author
has_many :link_posts, as: :author
end
class CreateTextPosts < ActiveRecord::Migration[6.1]
def change
create_table :text_posts do |t|
t.string :title
t.string :content
t.references :author, polymorphic: true
t.timestamps
end
end
end
class CreateLinkPosts < ActiveRecord::Migration[6.1]
def change
create_table :link_posts do |t|
t.string :title
t.string :url
t.references :author, polymorphic: true
t.timestamps
end
end
end
class CreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :name
t.timestamps
end
end
end
class CreateGuests < ActiveRecord::Migration[6.1]
def change
create_table :guests do |t|
t.timestamps
end
end
end
Console output:
:001 > user = User.create(name: 'alice')
(1.6ms) SELECT sqlite_version(*)
TRANSACTION (0.1ms) begin transaction
TRANSACTION (0.1ms) SAVEPOINT active_record_1
User Create (1.2ms) INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "alice"], ["created_at", "2021-06-11 23:33:38.445387"], ["updated_at", "2021-06-11 23:33:38.445387"]]
TRANSACTION (0.2ms) RELEASE SAVEPOINT active_record_1
:002'> text_post = TextPost.create(title: 'foo', content: 'lorem ipsum', author_id: 1, author_type:
'user')
Traceback (most recent call last):
1: from (irb):2:in `<main>'
NameError (wrong constant name user)
The names of constants look like the names of local variables, except that they begin with a capital letter.
All the built-in classes, along with the classes you define, have a corresponding global constant with the same name as the class called class name.
So in your case, when you define User class, there's a constant class name: User, but not user, that why the error NameError (wrong constant name user) is raised.
try text_post = TextPost.create(title: 'foo', content: 'lorem ipsum', author_id: 1, author_type: 'User')
I have a really dumb issue, I have two models:
class User < ApplicationRecord
belongs_to :role
end
and
class Role < ApplicationRecord
has_many :user
end
I'm trying to get the user and make a join with the role's table to get the role's name and id, like:
"user" : {
"name": "json",
"role": {"name":"admin", "id":1}
}
however, after using:
User.includes(:role).all
I just get the users with a "role_id" value, I've also tried:
User.joins(:role)
With the same result. I've been looking at the official docs at (https://guides.rubyonrails.org/active_record_querying.html) and it should be pretty straightforward but I don't know what I'm doing wrong. Do I need to add something to my migrations?, At my create_user migration I have:
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :username
t.string :email
t.integer :role_id
end
add_index :users, :email
add_index :users, :username
end
end
and my create_roles migration
class CreateRoles < ActiveRecord::Migration[6.0]
def change
create_table :roles do |t|
t.string :name
end
end
end
all by itself doesn't return such a custom data. You can use as_json for that:
User.all.as_json(only: :name, include: { role: { only: [:name, :id] } }, root: true)
# User Load (0.5ms) SELECT "users".* FROM "users"
# Role Load (0.6ms) SELECT "roles".* FROM "roles" WHERE "roles"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
# => [{"user"=>{"name"=>"json", "role"=>{"id"=>1, "name"=>"admin"}}}]
I'm trying to migrate my project from Rails 5.0 to 5.2
Project extensively uses creation of related models through .build_%related_model%, it was working on rails 5.0 and now it's broke.
Does this functionality removed, or should i use another syntax?
class User < ActiveRecord::Base
belongs_to :profile, inverse_of: :user
end
class Profile < ActiveRecord::Base
has_one :user, inverse_of: :profile
end
new_user = User.new
new_user.build_profile
new_user.save
Previously this code created both User and his Profile. Now this will create only User, without Profile.
Any ideas how to fix this?
irb(main):001:0> new_user = User.new
=> #<User id: nil, profile_id: nil, created_at: nil, updated_at: nil>
irb(main):002:0> new_user.build_profile
=> #<Profile id: nil, created_at: nil, updated_at: nil>
irb(main):003:0> new_user.save
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO "profiles" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2018-04-21 13:16:23.669286"], ["updated_at", "2018-04-21 13:16:23.669286"]]
Profile Load (0.2ms) SELECT "profiles".* FROM "profiles" WHERE "profiles"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
SQL (0.2ms) INSERT INTO "users" ("profile_id", "created_at", "updated_at") VALUES (?, ?, ?) [["profile_id", 1], ["created_at", "2018-04-21 13:16:23.691292"], ["updated_at", "2018-04-21 13:16:23.691292"]]
(181.1ms) commit transaction
=> true
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.integer :profile_id
t.timestamps
end
end
end
class CreateProfiles < ActiveRecord::Migration[5.1]
def change
create_table :profiles do |t|
t.timestamps
end
end
end
class User < ApplicationRecord
belongs_to :profile, inverse_of: :user
end
class Profile < ApplicationRecord
has_one :user, inverse_of: :profile
end
tested it out all worked. Copied code to answer since its not readable in comment. Your problem must be somewhere else do u get any errors or paste migration files to under question
accepts_nested_attributes_for :profile fixed this exact issue, but many other have popped up.
I had to stop this update and rollback everything.
In my Rails 5.1 app I am trying to create a tagging system from scratch.
I want Tags to be a polymorphic has_many :through association so that I can tag multiple models.
Currently I'm able to create a Tag (and the associated Tagging) in the console by doing: Note.last.tags.create(name: "example") which generates the correct SQL:
Note Load (0.2ms) SELECT "notes".* FROM "notes" ORDER BY "notes"."id" DESC LIMIT $1 [["LIMIT", 1]]
(0.2ms) BEGIN
SQL (0.4ms) INSERT INTO "tags" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "example"], ["created_at", "2017-10-21 14:41:43.961516"], ["updated_at", "2017-10-21 14:41:43.961516"]]
Note Load (0.3ms) SELECT "notes".* FROM "notes" WHERE "notes"."id" = $1 LIMIT $2 [["id", 4], ["LIMIT", 1]]
SQL (0.4ms) INSERT INTO "taggings" ("created_at", "updated_at", "tag_id", "taggable_id", "taggable_type") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["created_at", "2017-10-21 14:41:43.978286"], ["updated_at", "2017-10-21 14:41:43.978286"], ["tag_id", 9], ["taggable_id", 4], ["taggable_type", "Note"]]
But when trying to create a Tag and its associations through my form it doesn't work. I can create the Tag but no Tagging.
controllers/notes/tags_controller.rb
class Notes::TagsController < TagsController
before_action :set_taggable
private
def set_taggable
#taggable = Note.find(params[:note_id])
end
end
controllers/tags_controller.rb
class TagsController < ApplicationController
before_action :authenticate_user!
def create
#tag = #taggable.tags.new(tag_params)
#tag.user_id = current_user.id
if #tag.save
redirect_to #taggable, success: "New tag created."
else
render :new
end
end
private
def tag_params
params.require(:tag).permit(:name)
end
end
routes.rb
...
resources :notes, except: [:index] do
resources :tags, module: :notes
end
...
.
class Note < ApplicationRecord
belongs_to :notable, polymorphic: true
has_many :taggings, as: :taggable
has_many :tags, through: :taggings
end
class Tag < ApplicationRecord
has_many :taggings
has_many :taggables, through: :taggings
end
class Tagging < ApplicationRecord
belongs_to :tag
belongs_to :taggable, polymorphic: true
end
notes/show.html.erb
<p><%= #note.body %></p>
<%= render partial: 'tags/tags', locals: { taggable: #note } %>
<%= render partial: 'tags/form', locals: { taggable: #note } %>
tags/form.html.erb
<%= simple_form_for [taggable, Tag.new] do |f| %>
<%= f.input :name %>
<%= f.submit %>
<% end %>
The error might be that the Tagging is not getting saved due to the :tag association being required by default.
Try:
class Tagging < ApplicationRecord
belongs_to :tag, required: false
belongs_to :taggable, polymorphic: true
end
Your approach is fundamentially flawed in that it will create duplicates of each tag instead of creating a join record. It also adds unessicary complication in that you have to create nested controllers for each taggable resource.
The fact that this does not fail a uniqueness validation for tags.name shows a shortcoming in your application - you should have a unique index in the DB and a validation in the model to avoid duplicates.
This would be a perfectly fine approach for something like comments where each created record should be unique but is not for this case where you're linking to an indirect association.
To assign existing tags to a record you can use a select or checkboxes to pass an array of ids:
<%= form_for(#note) do |f| %>
# ...
<%= f.collection_checkboxes(:tags_ids, Tag.all, :id, :name) %>
<% end %>
To create new tags you can use nested attributes or use ajax to send a POST request to /tags and update the view so that the tag ends up in the list of checkboxes.
I have Rails API application with many to many relationship between users and projects though project_memberships table.
Models:
class User < ActiveRecord::Base
has_many :project_memberships, dependent: :destroy
has_many :projects, -> { uniq }, through: :project_memberships
accepts_nested_attributes_for :project_memberships, allow_destroy: true
end
class Project < ActiveRecord::Base
has_many :project_memberships, dependent: :destroy
has_many :users, -> { uniq }, through: :project_memberships
end
class ProjectMembership < ActiveRecord::Base
belongs_to :user
belongs_to :project
validates :user, presence: true
validates :project, presence: true
end
Controller:
class UsersController < ApplicationController
expose(:user, attributes: :user_params)
respond_to :json
# removed unrelated actions
def update
user.update user_params
respond_with user
end
private
def user_params
params.require(:user).permit(
:first_name, :last_name,
project_memberships_attributes:
[:id, :project_id, :membership_starts_at, :_destroy]
)
end
end
The problem is that when I send a PUT request to http://localhost:3000/users/1 with the following json:
{"user":{"project_memberships_attributes":[{"project_id": 1}]}
user.update user_params creates 2 ProjectMembership records with the same user_id and project_id.
SQL (0.3ms) INSERT INTO "project_memberships" ("project_id", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["project_id", 1], ["user_id", 1], ["created_at", "2016-03-18 18:00:07.670012"], ["updated_at", "2016-03-18 18:00:07.670012"]]
SQL (0.2ms) INSERT INTO "project_memberships" ("project_id", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["project_id", 1], ["user_id", 1], ["created_at", "2016-03-18 18:00:07.671644"], ["updated_at", "2016-03-18 18:00:07.671644"]]
(1.0ms) COMMIT
Btw destroying and updating already existing records by specifying id in nested attributes works correctly.
The first step you need to take is to ensure uniqueness on the database level:
class AddUniquenessConstraintToProjectMemberships < ActiveRecord::Migration
def change
# There can be only one!
add_index :project_memberships, [:user, :project], unique: true
end
end
This avoids race conditions that would occur if we relied on ActiveRecord alone.
From Thoughtbot: The Perils of Uniqueness Validations.
You then want to add an application level validation to avoid the ugly DB driver exceptions that occur if you violate the constraint:
class ProjectMembership < ActiveRecord::Base
belongs_to :user
belongs_to :project
validates :user, presence: true
validates :project, presence: true
validates_uniqueness_of :user_id, scope: :project_id
end
You can then remove the -> { uniq } lambda on your associations as you have taken the proper steps to ensure uniqueness.
The rest of your issues are due to a misunderstanding of how accepts_nested_attributes_for works:
For each hash that does not have an id key a new record will be
instantiated, unless the hash also contains a _destroy key that
evaluates to true.
So {"user":{"project_memberships_attributes":[{"project_id": 1}]} will always create a new record if you do not have proper uniqueness validations.