在相同的 2 个对象之间有 2 个不同的关联通常是不好的做法吗?

Is it generally bad practice to have 2 different associations between the same 2 objects?

我意识到这有点自以为是,所以特别询问这是否通常是每个代码约定的好主意。

示例场景

建模图书馆中 BooksClients 之间的关系,我们假设根据图书馆规则,客户一次只能借阅一本书。

让我感到复杂的是这样一个事实,即客户只在给定的时间段内签出一本书,然后 return 就可以了。所以这几乎就像,你不希望这种关系永远持续下去......在某个时候,客户解除了那本书的责任(例如,如果一本书在客户拥有 return编辑它,你不想打电话给过去的客户要求更换)。

然而,虽然它是暂时的,但您仍然需要一份关系记录,以便您可以跟踪签出一本书的客户的历史记录(因为在线下,也许您确实想向过去发送调查要求评论的客户)。

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

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

2 个关联会是什么样子

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

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

这个关系非常有用,如果客户打电话说他们忘记了从他们的个人书架上 return 要哪本书,你可以打电话给 @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 找到合适的人跟进。

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

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

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;

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