Rails Has Many Through Relationship - Difference Between New-Save and Create - ruby-on-rails

I checked the Rails Guides about has_many through relationship but they have a poor documentation for has_many through's effects in controllers in views.
The models:
class Project < ActiveRecord::Base
has_many :twitter_memberships
has_many :twitter_accounts, through: :twitter_memberships
end
class TwitterAccount < ActiveRecord::Base
has_many :twitter_memberships
has_many :projects, through: :twitter_memberships
end
class TwitterMembership < ActiveRecord::Base
belongs_to :project
belongs_to :twitter_account
end
Routes:
resources :projects do
resources :twitter_accounts
end
The question is, when I use the create method, a TwitterMembership object is being created automatically but when I use new method and then the save method, TwitterMembership is not being created. Many of the posts in Stackoverflow is saying that create = new & save but it seems that they are not the same.
For example:
Project.first.twitter_accounts.create(name: "bar")
is creating both TwitterAccount and TwitterMembership:
SQL (0.5ms) INSERT INTO "twitter_accounts" ("created_at", "name", "updated_at") VALUES (?, ?, ?) [["created_at", Thu, 20 Nov 2014 22:01:06 UTC +00:00], ["name", "test_record"], ["updated_at", Thu, 20 Nov 2014 22:01:06 UTC +00:00]]
SQL (0.3ms) INSERT INTO "twitter_memberships" ("created_at", "project_id", "twitter_account_id", "updated_at") VALUES (?, ?, ?, ?) [["created_at", Thu, 20 Nov 2014 22:01:06 UTC +00:00], ["project_id", 1], ["twitter_account_id", 8], ["updated_at", Thu, 20 Nov 2014 22:01:06 UTC +00:00]]
But when I do the same thing in the controllers/twitter_accounts_controller with new & create - it's not creating the TwitterMembership:
def new
#twitter_account = #project.twitter_accounts.new
end
def create
#twitter_account = #project.twitter_accounts.new(twitter_account_params)
#twitter_account.save
end
Can you explain me the difference between new-save and create? And is it okay if I use create method directly in my controller like this?
def create
#twitter_account = #project.twitter_accounts.create(twitter_account_params)
end
Thanks in advance.
Edit. Here is the full controller =>
class TwitterAccountsController < ApplicationController
before_action :set_project
before_action :set_twitter_account, only: [:show, :edit, :update, :destroy]
def new
#twitter_account = #project.twitter_accounts.new
end
def create
#twitter_account = #project.twitter_accounts.new(twitter_account_params)
#twitter_account.save
end
private
def set_project
#project = Project.friendly.find(params[:project_id])
end
def set_twitter_account
#twitter_account = TwitterAccount.friendly.find(params[:id])
end
def twitter_account_params
params.require(:twitter_account).permit(:name, :account_id)
end
end

The Fix
You need to set your inverse relationships on your models to guarantee the build and new on associations will work consistently to setup the relationships, foreign keys, and intermediate associations:
class Project < ActiveRecord::Base
has_many :twitter_memberships, inverse_of: :project
has_many :twitter_accounts, through: :twitter_memberships
end
class TwitterMembership < ActiveRecord::Base
belongs_to :project, inverse_of: :twitter_memberships
belongs_to :twitter_account, inverse_of: :twitter_memberships
end
class TwitterAccount < ActiveRecord::Base
has_many :twitter_memberships, inverse_of: :twitter_account
has_many :projects, through: :twitter_memberships
end
Which should then make this work
#twitter_account = Project.first.twitter_accounts.new(name: 'baz')
#twitter_account.save
And voila:
SQL (0.3ms) INSERT INTO "twitter_accounts" ("created_at", "name", "updated_at") VALUES (?, ?, ?) [["created_at", "2014-11-21 19:19:06.323072"], ["name", "baz"], ["updated_at", "2014-11-21 19:19:06.323072"]]
SQL (0.1ms) INSERT INTO "twitter_memberships" ("created_at", "project_id", "twitter_account_id", "updated_at") VALUES (?, ?, ?, ?) [["created_at", "2014-11-21 19:19:06.324779"], ["project_id", 1], ["twitter_account_id", 7], ["updated_at", "2014-11-21 19:19:06.324779"]]
Also, using create is perfectly acceptable assuming you've handled all your edge cases in the event of a failure. For example: if it fails do you show an error to the end user? If it is not supposed to fail, you should raise an exception (see create!) to generate a notification of some sort to inform you of the failure.
The Why
In short, without the inverse_of setup on has_many, has_one, and belongs_to Rails doesn't completely understand the chain of intermediate models that glue things together. Rails will in certain cases (like create) take a "best guess", and may get things right. Rails Guide doesn't make this clear, but the documentation on inverse_of spells this out.
It has become a Rails idiom to set inverse_of on all has_many, has_one, and belongs_to relationships, and should be done to ensure guessing by Rails is not necessary.

Related

Create model with it's relations

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.

Using `assign_attributes` saves `has_many through:` association immediately

As far as I know, assign_attributes (unlike update_attributes) is not supposed to save the record or for that matter, any record.
So it quite startled me when I discovered that this is not true when supplying _ids for a has_many through: relation.
Consider the following example:
class GroupUser < ApplicationRecord
belongs_to :group
belongs_to :user
end
class Group < ApplicationRecord
has_many :group_users
has_many :users, through: :group_users
end
class User < ApplicationRecord
has_many :group_users
has_many :groups, through: :group_users
validates :username, presence: true
end
So we have users and groups in an m-to-m relationship.
Group.create # Create group with ID 1
Group.create # Create group with ID 2
u = User.create(username: 'Johny')
# The following line inserts two `GroupUser` join objects, despite the fact
# that we have called `assign_attributes` instead of `update_attributes`
# and, equally disturbing, the user object is not even valid as we've
# supplied an empty `username` attribute.
u.assign_attributes(username: '', group_ids: [1, 26])
The log as requested by a commenter:
irb(main):013:0> u.assign_attributes(username: '', group_ids: [1, 2])
Group Load (0.2ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" IN (1, 2)
Group Load (0.1ms) SELECT "groups".* FROM "groups" INNER JOIN "group_users" ON "groups"."id" = "group_users"."group_id" WHERE "group_users"."user_id" = ? [["user_id", 1]]
(0.0ms) begin transaction
SQL (0.3ms) INSERT INTO "group_users" ("group_id", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["group_id", 1], ["user_id", 1], ["created_at", "2017-06-29 08:15:11.691941"], ["updated_at", "2017-06-29 08:15:11.691941"]]
SQL (0.1ms) INSERT INTO "group_users" ("group_id", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["group_id", 2], ["user_id", 1], ["created_at", "2017-06-29 08:15:11.693984"], ["updated_at", "2017-06-29 08:15:11.693984"]]
(2.5ms) commit transaction
=> nil
I daresay that update_attributes and the _ids construct are mostly used for processing web forms - in this case a form that updates the user itself as well as its group association. So I think it is quite safe to say that the general assumption here is all or nothing, and not a partial save.
Am I using it wrong in some way?
#gokul-m suggests reading about the issue at https://github.com/rails/rails/issues/17368. One of the comments in there points to a temporary workaround: https://gist.github.com/remofritzsche/4204e399e547ff7e3afdd0d89a5aaf3e
an example of my solution to this problem:
ruby:
def assign_parameters(attributes, options = {})
with_transaction_returning_status {self.assign_attributes(attributes, options)}
end
You can handle validation with assign_attributes like so
#item.assign_attributes{ year: "2021", type: "bad" }.valid?

has_many relationship, accepts_nested_attributes_for - nested record not created

I am trying to create a relation_level, with a score attribute, for each interest_question/feed pair. I am doing this with an accepts_nested_attributes_for :relation_levels in the feed form. Everything renders as it should, and when the form is submitted, a feed is created, but no relation_levels are created.
I've also tried adding the feed_id as a hidden field in the form.
(using rails 4 and haml)
app/models/interest_question.rb
class InterestQuestion < ActiveRecord::Base
has_many :relation_levels, dependent: :destroy
has_many :interest_answers, dependent: :destroy
end
app/models/relation_level.rb
class RelationLevel < ActiveRecord::Base
belongs_to :feed
belongs_to :interest_question
end
app/models/feed.rb
class Feed < ActiveRecord::Base
has_many :relation_levels, dependent: :destroy
has_many :interest_questions, through: :relation_levels
accepts_nested_attributes_for :relation_levels
end
app/views/feeds/_form.html.haml
=form_for(#feed) do |f|
...other fields
...other fields
-#interest_questions.each do |iq|
=f.fields_for #feed.relation_levels.build(interest_question_id: iq.id) do |rl|
=rl.label iq.question_text
=rl.range_field :score, max: 100, min: 0, default: 0
=f.submit
app/controllers/feeds_controller.rb
class FeedsController < ApplicationController
def new
#feed = Feed.new
#sources = Source.all
#interest_questions = InterestQuestion.all
end
def create
#feed = Feed.new(feed_params)
if #feed.save
redirect_to '/feeds', notice: 'Feed created.'
else
render action: 'new'
end
end
...
private
def feed_params
params.require(:feed).permit(..., relation_levels_attributes:
[:interest_question_id, :score, :feed_id])
end
Server output:
Started POST "/feeds" for 127.0.0.1 at 2013-12-30 15:00:58 -0600
Processing by FeedsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"/+TOOdpZZk85YvVlxkRIpLNfPfVVtGUTlKPb9Ctkvh8=", "feed"=>{"url"=>"lkas6df.com", "source_id"=>"1", "section"=>"kasl6d6fa", "area_importance"=>"", "is_local_news"=>"0", "relation_level"=>{"score"=>"17"}}, "commit"=>"Create Feed"}
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."remember_token" = '6fcabf7c7b1376250b1ffa589ff4f2279854d066' LIMIT 1
Unpermitted parameters: relation_level
(0.1ms) begin transaction
Source Load (0.1ms) SELECT "sources".* FROM "sources" WHERE "sources"."id" = ? ORDER BY "sources"."id" ASC LIMIT 1 [["id", 1]]
Feed Exists (0.2ms) SELECT 1 AS one FROM "feeds" WHERE "feeds"."url" = 'lkas6df.com' LIMIT 1
SQL (1.7ms) INSERT INTO "feeds" ("created_at", "section", "source_id", "updated_at", "url") VALUES (?, ?, ?, ?, ?) [["created_at", Mon, 30 Dec 2013 21:00:58 UTC +00:00], ["section", "kasl6d6fa"], ["source_id", 1], ["updated_at", Mon, 30 Dec 2013 21:00:58 UTC +00:00], ["url", "lkas6df.com"]]
(170.8ms) commit transaction
Redirected to http://localhost:3000/feeds
I've been stuck on this for a while, thanks in advance for any help :)
This should be a comment, but it's too big:
Maybe this could be your issue?
=f.fields_for #feed.relation_levels.build(interest_question_id: iq.id) do |rl|
This builds an ActiveRecord object on the fly, which IMO is bad practice. I'd build the object in the controller, and then call the object in the f.fields_for, like this:
#app/controllers/feeds_controller.rb
def new
#feed = Feed.new
#sources = Source.all
#interest_questions = InterestQuestion.all
#interest_questions.count.times do
#feed.relation_levels.build
end
end
You can then call:
=f.fields_for :relation_levels do |rl|
Much cleaner, and will likely help with your debugging!

ActiveRecord has_one having conditions does not set condition attributes on creation

Using Rails 3.2.11 and ruby 1.9.3:
I have Review, User and ReviewAccess classes
class ReviewAccess < ActiveRecord::Base
belongs_to :user
belongs_to :review
attr_accessible :role_id
end
class Review < ActiveRecord::Base
has_one :review_accesses_owner, :class_name => 'ReviewAccess',
:conditions => "review_accesses.role_id = 1"
has_one :owner, :class_name => 'User', :through => :review_accesses_owner,
:source => :user
end
Basically Review is many-to-many with User and the join table is ReviewAccess where it additionally holds the relation role (1 for owner) in role_id column.
I can read the the review owner by:
Review.owner # works
# sql: SELECT "review_accesses".* FROM "review_accesses" WHERE "review_accesses"."review_id" = 7 AND (review_accesses.role_id = 1) LIMIT 1
However, setting the owner does not work because it doesn't set role_id to 1 (as stated in the conditions clause of the association)
Review.owner = current_user # does not set role_id
# sql: INSERT INTO "review_accesses" ("created_at", "review_id", "role_id", "updated_at", "user_id") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["created_at", Sun, 27 Oct 2013 08:02:54 UTC +00:00], ["review_id", 7], ["role_id", nil], ["updated_at", Sun, 27 Oct 2013 08:02:54 UTC +00:00], ["user_id", 1]]
I know I can override owner= but I have many of these (for each role) and I want to use the association DSL instead.
How to update associations having conditions to set the conditions on creation?
I'm a little bit afraid that using DSL it's not a good choice.
Prefer overriding accesorrs than putting code into callbacks if you need add some custom behavior.
To some generic, repetitive methods use metaprogramming stuff. If you'll have many roles you'll have DRY in your association definitions still, so problem is the same but in different place.
To sum up, override writer by using metaprogramming for it. Anywhere you have an array of roles, right?

Rails 3 ActiveRecord has_many polymorphic - Why isn't the id of the first object being saved for insertion into the join table?

Wise people,
Please help me troubleshoot my has_many polymorphic object creation problem.
I have a polymorphic has_many relationship between two objects with a join table in the middle. Acronyms (and other objects) on one side, Categories on the other. In between them, I have a join object between the two objects.
I cannot successfully create an acronym with a category. I can, however, create an acronym without a category, then add the category to the existing acronym.
My model objects look like the following code:
class Acronym < ActiveRecord::Base
has_many :category_belongings, :as => :categorizable, :dependent => :delete_all
has_many :categories, :through => :category_belongings
end
class Category < ActiveRecord::Base
has_many :category_belongings, :dependent => :delete_all
has_many :acronyms, :through => :category_belongings, :source => :categorizable, :source_type => 'Acronym'
end
class CategoryBelonging < ActiveRecord::Base
belongs_to :categorizable, :polymorphic => true
belongs_to :category
end
For reference, I removed some extra fields and irrelevant rules. I also refactored my code to look just like the example here:
Setting up a polymorphic has_many :through relationship
Through my Rails application/rails console, I am able to:
Successfully create an acronym without any categories
Successfully create a category
Successfully add an existing category to an existing acronym through updating the acronym
I cannot, however, create a new acronym with an existing category. I can see in the logs Rails tries to do the following:
Gathers the parameters from the post. These parameters include the columns on the Acronym table as well as the category IDs
Checks to see if the Category exists. It does.
Checks to see if the Acronym exists. It does not.
Inserts a new Acronym into the database with all the column parameters
Attempts to insert a new entry in the join table. This is where the error is. The SQL for this insertion contains a Nil value for the Acronym ID.
Rolls my changes back.
Processing by Admin::AcronymsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"/uquz5FvtMh0QWP5NoWwTO9FMMEC9rsMTrTj4WUNxxE=", "acronym"=>{"name"=>"A Test Acronym", "definition"=>"A Test Definition", "explanation"=>"", "category_ids"=>["1", ""], "state"=>"unapproved"}, "commit"=>"Create Acronym"}
Category Load (0.4ms) SELECT "categories".* FROM "categories" WHERE "categories"."id" = $1 LIMIT 1 [["id", 1]]
(0.3ms) BEGIN
Category Exists (0.6ms) SELECT 1 AS one FROM "categories" WHERE ("categories"."name" = 'Internet' AND "categories"."id" != 1) LIMIT 1
Acronym Exists (26.0ms) SELECT 1 AS one FROM "acronyms" WHERE "acronyms"."name" = 'A Test Acronym' LIMIT 1
SQL (18.1ms) INSERT INTO "acronyms" ("definition", "explanation", "improvement_reason", "likes_count", "name", "state", "submitter_id") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id" [["definition", "A Test Definition"], ["explanation", ""], ["improvement_reason", nil], ["likes_count", 0], ["name", "A Test Acronym"], ["state", "unapproved"], ["submitter_id", nil]]
SQL (55.8ms) INSERT INTO "category_belongings" ("categorizable_id", "categorizable_type", "category_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["categorizable_id", nil], ["categorizable_type", "Acronym"], ["category_id", 1], ["created_at", Thu, 28 Feb 2013 23:03:48 UTC +00:00], ["updated_at", Thu, 28 Feb 2013 23:03:48 UTC +00:00]]
PG::Error: ERROR: null value in column "categorizable_id" violates not-null constraint
: INSERT INTO "category_belongings" ("categorizable_id", "categorizable_type", "category_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"
(0.3ms) ROLLBACK
I would assume on the Acronym insert, an acronym_id would be created, but I don't see any output indicating what the value of that acronym_id is. To me, the error is clearly that the acronym_id is Nil when I try to insert the new row into category_belongings. Do I need to somehow point acronym_id to categorizable_id? If that's the case, how would I do this?
I'm new to rails, and I am trying to maintain and improve an existing system. Thanks for any help.
Based on what you are looking to do, you need to use accepts_nested_attributes_for. This will allow you to create a category when you create an acronym. Here is how you should set it up.
class Acronym < ActiveRecord::Base
has_many :categories, :through => :category_belongings
has_many :category_belongings, :dependent => :destroy
accepts_nested_attributes_for :acronyms
attr_accessible :acronyms_attributes
end
class Category < ActiveRecord::Base
has_many :acronyms, :through => :category_belongings
has_many :category_belongings, :dependent => :destroy
end
class CategoryBelonging < ActiveRecord::Base
belongs_to :acronym
belongs_to :category
end
This setup will allow you to nest your forms and create a category when you create an acronym. To read more about all the options accepts_nested_attributes_for has checkout the Rails API. This Railscast also has some good information on nested forms.
It seems my code was fine - I got bit by a Rails bug. I solved the problem by moving my rails version to 3.2.8 with the security patch enabled according to these instructions. After that, everything worked fine with no code changes necessary.
https://github.com/rails/rails/issues/8269#issuecomment-10518099

Resources