简体   繁体   中英

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM