使用2个相关模型创建和更新模型

Ale*_*bio 9 ruby-on-rails ruby-on-rails-4

我有这个型号

class Book < ActiveRecord::Base
  has_many :accountings
  has_many :commodities, through: :accountings
end

class Accounting < ActiveRecord::Base
  belongs_to :book
  belongs_to :commodity
end

class Commodity < ActiveRecord::Base
  has_many :accountings
end
Run Code Online (Sandbox Code Playgroud)

我的书形式是:

<%= form_for(book) do |f| %>
  <%= render 'shared/error_messages', object: book %>
  <%= f.label :name %>
  <%= f.text_field :name %>
  <%= f.label :commodity_ids %>
  <%= f.collection_select :commodity_ids, Commodity.all, :id, :name, {}, {multiple: true} %>
  <br />
  <%= f.submit class: 'btn btn-primary' %>
<% end %>
Run Code Online (Sandbox Code Playgroud)

在BooksController:

  def new
    @book = Book.new
  end

  def create
    @book = Book.new(book_params)
    if @book.save
      redirect_to @book
    else
      render 'new'
    end
  end

  def edit
    @book = Book.find(params[:id])
  end

  def update
    @book = Book.find(params[:id])
    if @book.update_attributes(book_params)
      redirect_to @book
    else
      render 'edit'
    end
  end

  private

    def book_params
      params.require(:book).permit(:name, commodity_ids: [])
    end
Run Code Online (Sandbox Code Playgroud)

这一切都很好.并且Accountingcommodity_ids更新时添加和删除记录.(1)

现在我需要添加一个新模型:Company由于Book并且Commodity由所有公司共享,Accounting必须属于company,并且不在所有系统中共享.并Accounting成为:

class Accounting < ActiveRecord::Base
  belongs_to :book
  belongs_to :commodity
  belongs_to :company
end
Run Code Online (Sandbox Code Playgroud)

Company模型:

class Company < ActiveRecord::Base
  has_many :accountings
end
Run Code Online (Sandbox Code Playgroud)

Accounting 它不仅仅是商品和书籍(和公司)之间的关系,它也代表了一种商业模式.

这里的约束是:当a Commodity添加到a时Book,Accounting必须为每个 创建一个new Company.(2)

我确实尝试Bookcompanies,通过accountings.但是,它不起作用.即使它不代表商业模式,书也不关心公司,我认为Book是链接模型的好人选.(3)

现在我正在考虑添加一个新模型BookCommodity,通过这个模型将书籍和商品联系起来,并且在保存时,这个新模型会生成Accounting所有人需要的记录companies.(4)

在添加第五个模型之前,我想问你是否有更好的方法来管理这些东西?

编辑

在Github,你可以找到一个demo_finance项目,只有这篇文章的代码.它有4个分支:

  • master:第一个版本,在添加公司模型之前,当然它工作正常.(1)
  • with_companies:具有公司模型的分支,以及会计模型所需的约束.(2)这是任何变化的起点.
  • first_attempt:带有尝试的with_companies分支:(3).这是行不通的.
  • second_attempt:with_companies分支尝试:(4).有用.它在创建会计创建时有一个回调,但在Books#update中需要更多的商品删除回调.我对这种方法并不完全满意.

这里的关键任务是在书中添加或删除商品,并更新帐户,就像它在版本(1)(主分支)中工作一样.

编辑#2

我试着这样做:

class Book < ActiveRecord::Base
  has_many :accountings
  has_many :commodity_companies, through: :accountings
  has_many :commodities, through: :commodity_companies
end
Run Code Online (Sandbox Code Playgroud)

但它不起作用.在更新它提出:

ActiveRecord::HasManyThroughNestedAssociationsAreReadonly
  Cannot modify association 'Book#commodities' because it goes through more than one other association.
Run Code Online (Sandbox Code Playgroud)

我也尝试这样做:

class Book < ActiveRecord::Base
  has_many :accountings do
    def build(attributes = {}, &block)
      Company.all.each do |company|
        @association.build(attributes.merge(company_id: company.id), &block)
      end
    end
  end
  has_many :commodities, through: :accountings
end
Run Code Online (Sandbox Code Playgroud)

但是,这个构建操作不会在更新书上调用commodity_ids=.

Ale*_*bio 0

回答我自己的问题:

我还没有选择这个答案作为正确的答案。因为,尽管它有效,但我认为可能有更好的答案,值得赏金。

我让它覆盖了该方法commodity_ids=,还需要添加dependent: :destroy会计选项和-> {uniq}商品范围。

class Book < ActiveRecord::Base
  has_many :accountings, dependent: :destroy
  has_many :commodities, ->{ uniq }, through: :accountings

  def commodity_ids=(ids)
    self.accountings = Commodity.where(id: ids).map do |commodity|
      Company.all.map do |company|
        accountings.find_by(company_id: company.id, commodity_id: commodity.id) ||
          accountings.build(company: company, commodity: commodity)
      end
    end.flatten
  end

  def commodities=(records)
    self.commodity_ids = records.map(&:id)
  end
end

class Commodity < ActiveRecord::Base
end

class Company < ActiveRecord::Base
end

class Accounting < ActiveRecord::Base
  belongs_to :book
  belongs_to :commodity
  belongs_to :company

  def to_s
    "#{book.name} - #{company.name} - #{commodity.name}"
  end
end
Run Code Online (Sandbox Code Playgroud)

从头开始在控制台运行:

~/ (main) > Book.create name: 'Book 1'
=> #<Book id: 1, name: "Book 1", created_at: "2015-09-30 11:47:23", updated_at: "2015-09-30 11:47:23">
~/ (main) > Commodity.create name: 'Commodity 1'
=> #<Commodity id: 1, name: "Commodity 1", created_at: "2015-09-30 11:47:37", updated_at: "2015-09-30 11:47:37">
~/ (main) > Commodity.create name: 'Commodity 2'
=> #<Commodity id: 2, name: "Commodity 2", created_at: "2015-09-30 11:47:40", updated_at: "2015-09-30 11:47:40">
~/ (main) > Commodity.create name: 'Commodity 3'
=> #<Commodity id: 3, name: "Commodity 3", created_at: "2015-09-30 11:47:42", updated_at: "2015-09-30 11:47:42">
~/ (main) > Company.create name: 'Company 1'
=> #<Company id: 1, name: "Company 1", created_at: "2015-09-30 11:47:51", updated_at: "2015-09-30 11:47:51">
~/ (main) > Company.create name: 'Company 2'
=> #<Company id: 2, name: "Company 2", created_at: "2015-09-30 11:47:54", updated_at: "2015-09-30 11:47:54">
~/ (main) > bb = Book.first
=> #<Book id: 1, name: "Book 1", created_at: "2015-09-30 11:47:23", updated_at: "2015-09-30 11:47:23">
~/ (main) > bb.commodity_ids = ['', nil, 1, '3']
=> [
    [0] "",
    [1] nil,
    [2] 1,
    [3] "3"
]
~/ (main) > bb.save
=> true
~/ (main) > bb.reload
=> #<Book id: 1, name: "Book 1", created_at: "2015-09-30 11:47:23", updated_at: "2015-09-30 11:47:23">
~/ (main) > bb.accountings.map(&:to_s)
=> [
    [0] "Book 1 - Company 1 - Commodity 1",
    [1] "Book 1 - Company 2 - Commodity 1",
    [2] "Book 1 - Company 1 - Commodity 3",
    [3] "Book 1 - Company 2 - Commodity 3"
]
~/ (main) > bb.commodities.map(&:name)
=> [
    [0] "Commodity 1",
    [1] "Commodity 3"
]
~/ (main) > bb.commodity_ids = ['']
=> [
    [0] ""
]
~/ (main) > bb.save
=> true
~/ (main) > bb.reload
=> #<Book id: 1, name: "Book 1", created_at: "2015-09-30 11:47:23", updated_at: "2015-09-30 11:47:23">
~/ (main) > bb.commodities.map(&:name)
=> []
~/ (main) > bb.accountings.map(&:to_s)
=> []
~/ (main) > 
Run Code Online (Sandbox Code Playgroud)