Reputation: 383
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
Upvotes: 1
Views: 296
Reputation: 36
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.
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.
Upvotes: 2