在相同的 2 个对象之间有 2 个不同的关联通常是不好的做法吗?
Is it generally bad practice to have 2 different associations between the same 2 objects?
我意识到这有点自以为是,所以特别询问这是否通常是每个代码约定的好主意。
示例场景
建模图书馆中 Books
和 Clients
之间的关系,我们假设根据图书馆规则,客户一次只能借阅一本书。
让我感到复杂的是这样一个事实,即客户只在给定的时间段内签出一本书,然后 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;
在两个相同的对象之间建立多个关联是一种不好的做法吗?不一定。它是解决很多问题的合理解决方案,但也有其自身的一些缺点。
我意识到这有点自以为是,所以特别询问这是否通常是每个代码约定的好主意。
示例场景
建模图书馆中 Books
和 Clients
之间的关系,我们假设根据图书馆规则,客户一次只能借阅一本书。
让我感到复杂的是这样一个事实,即客户只在给定的时间段内签出一本书,然后 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;
在两个相同的对象之间建立多个关联是一种不好的做法吗?不一定。它是解决很多问题的合理解决方案,但也有其自身的一些缺点。