【问题标题】:Is it generally bad practice to have 2 different associations between the same 2 objects?在相同的 2 个对象之间有 2 个不同的关联通常是不好的做法吗?
【发布时间】:2020-05-09 02:22:20
【问题描述】:

我意识到这有点固执己见,因此特别询问这是否是每个代码约定的好主意。

示例场景

在图书馆中为BooksClients 之间的关系建模,假设根据图书馆规则,客户一次只能签出一本书。

让我感到复杂的是,客户只在给定的时间段内签出一本书,然后将其退回。所以这几乎就像,你不希望这种关系永远持续下去……在某个时候,客户免除了那本书的责任(例如,如果一本书在客户退回 2 年后损坏了,你不想打电话给过去的客户要求更换)。

尽管它是暂时的,但您仍然需要记录这种关系,以便您可以跟踪查看过一本书的客户的历史记录(因为下线,也许您确实想向过去的客户发送调查问卷供评论)。

换句话说,在我看来,有两种关联……一种是暂时的,一种是更持久的。

虽然我可以在技术上编写代码来完成这项工作,但这似乎是一种糟糕的做法(就像......直觉上我觉得这个问题的答案是肯定的......这不是一个好的做法)。但我想确认一下,如果是这样,还有什么其他解决方案可以解决这个问题(has_many :through 可能通过表具有某种...current_rental 属性?)

两个关联的样子

Association #1,临时关联,你想知道一本书是借给哪个客户的

Client has_one :book, foreign_key: "current_id"
Book belongs_to :current_client, class_name: "Client", foreign_key: "current_id"

这种关系非常有用,如果客户打电话说他们忘记了要从个人书架归还哪本书,您可以致电@client.book.title

关联 #2,长期关联,您想知道一本书的历史(进一步假设客户的历史并不重要,如果这样做,这将更清楚地是 has_many :through 或 HABTM 关系)

Client belongs_to :checkout_book, class_name: "Book", foreign_key: "book_id"
Book has_many :clients, foreign_key: "book_id"

通过这种关系,您可以查看一本书的历史、有多少客户已将其签出到@book.clients.size,或者您可以通过电子邮件向他们发送电子邮件以进行调查@book.clients.map(&:email)

然后,这种分离消除了混淆,例如需要更换书籍,调用 @book.clients.last 可能会检索到不正确的客户,因为预订是提前进行的,因此结帐不一定按顺序进行。不过,您可以@book.current_client 让合适的人跟进。

【问题讨论】:

  • 考虑模型是Users M-M Books。然后,“当前书”只是唯一一本书(如果以后允许的话,可以是多本书)。这使数据库模型保持一致,而无需其他技巧。无论数据库映射如何,代码计算属性总是可以返回“单一”书。返回书籍可能会更新加入关系实体的属性,或者可能导致关系被完全删除。
  • 但是您仍然需要一些其他属性才能确定该多对多关系中的状态checked out 对吗?我想这是看待它的一种方式。一种解决方案是拥有这样一个属性,而我的解决方案没有属性但使用关系
  • 也许在这种情况下,您可能会质疑关系的完整性。客户真的只有一本书吗?客户可以拥有的书籍总量不应受到限制。一次的图书总量是您想要限制的。您可以将数组属性设置为客户端已签出的书籍的名称,从而将签出限制限制为该数组的长度。在这种情况下,客户甚至可以出租多本书,这是非常合理的。
  • @james 实际上,是的(假设加入关系不是暂时的——在这种情况下,仅存在表示已签出)。我喜欢使用可导出的值,例如签入和签出日期。这也将允许多个结帐历史等。

标签: ruby-on-rails activerecord associations


【解决方案1】:

您想要存储额外关联的一个原因是,如果您有一个包含大量数据的联接表,则可以优化读取查询。

假设我们假设的图书馆非常受欢迎,每本书有数千笔借阅。我们的设置如下:

class Client
  has_many :loans
  has_many :books, through: :loans
end
class Book
  has_many :loans
  has_many :clients, through: :loans
end
class Loan
  belongs_to :book
  belongs_to :client
end
class LoansController ApplicationController
  def create
    @book = Books.find(id: params[:book_id])
    @loan = @book.loans.new(client: current_client)
    if @loan.save
      redirect_to @loan
    else
      render :new           
    end
  end
end

我们要求列出 100 本最受欢迎的书籍以及当前拥有该书籍的客户:

class BooksController < ApplicationController
  def by_popularity
     @book = Book.order(popularity: :desc).limit(100)
  end
end 
<% @books.each do |book| %>
  <tr>
    <td><%= link_to book.title, book %></td>
    <td><%= link_to book.clients.last.name, book.clients.last %></td> 
  <tr>
<% end %> 

由于这会导致 N+1 查询,我们得到 101 个数据库查询。不好。所以我们把includes扔在上面来解决它。

@book = Book.includes(:clients)
            .order(popularity: :desc)
            .limit(100)

一切都好,对吧?没有。对于我们从书籍加载的每条记录,这将加载并实例化来自贷款和客户的所有连接记录。由于这是成千上万,服务器在内存不足时会突然停止。

虽然有各种技巧,例如子查询来限制输出,但最简单且迄今为止最高效的解决方案是:

class Book
  has_many :loans, after_add: :set_current_client
  has_many :clients, through: :loans
  belongs_to :current_client, class_name: 'Client'

  def set_current_client(client)
    update_column(current_client_id: current_client)
  end
end
class BooksController < ApplicationController
  def by_popularity
     @book = Book.order(popularity: :desc)
                 .eager_load(:current_client)
                 .limit(100)
  end
end 
<% @books.each do |book| %>
  <tr>
    <td><%= link_to book.title, book %></td>
    <td><%= link_to book.current_client.name, book.current_client %></td> 
  <tr>
<% end %> 

TLDR;

在两个相同的对象之间建立多个关联是一种不好的做法吗?不一定。它是解决很多问题的合法解决方案,但也有其自身的一些缺点。

【讨论】:

  • 在这个 ex 中,Book belongs_to :current_client", since Rails can't guess this model based on name, you'd need to add class: "Client", foreign_key: "current_client_id", right? And the counterpart on Client would be has_one :book, foreign_key: "current_client_id"`,对吗?抱歉,如果您没有写下所有内容,因为它已经在我的假设答案中并且您正在精简。我只是在仔细检查
  • 没错,这是我的疏忽。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-09-09
  • 2012-06-17
  • 2016-01-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多