如何将查询可借图书的SQL正确转换为Rails ActiveRecord代码
你之前写的where.not(id: CheckoutLog.where(returned_date: nil))没有生效,本质是Rails的嵌套关系字段映射逻辑和你的预期不一致:
当你直接把一个CheckoutLog的查询结果集作为条件传入where.not时,Rails默认会取CheckoutLog模型的主键字段(也就是checkout_logs.id)去匹配books表的id字段,而非你原生SQL中要求的checkout_logs.book_id字段。这种错配下,查询是拿books的id和checkout_logs的自增主键做匹配,和“图书是否有未归还记录”的业务逻辑完全无关,自然无法起到过滤作用。
另外你之前把取单条记录的方法从.take改成.find(1)也存在逻辑偏差:.find(1)的行为是在查询结果集中筛选主键id=1的记录,而非按照查询条件取匹配到的第一条记录,和你原生SQL中limit 1的语义并不完全等价。
写法1:完全对齐原生SQL逻辑的写法
显式指定子查询需要返回的字段,确保和你最初的原生SQL生成完全一致的执行逻辑:
@book = Book .where(book_number: params[:book_number]) .where.not(id: CheckoutLog.where(returned_date: nil).select(:book_id)) .first
加上.select(:book_id)后,子查询会严格按照需求返回所有未归还借阅记录对应的book_id,和原SQL的子查询逻辑完全一致。你可以通过在查询末尾加.to_sql核对生成的SQL语句,确认和预期一致。
写法2:基于模型关联的更优Rails写法
结合schema中已经定义的外键关联,先在模型中补全标准关联定义:
# app/models/book.rb class Book < ApplicationRecord has_many :checkout_logs # 定义可借图书的作用域,逻辑为:不存在未归还的借阅记录 scope :available, -> { where.missing(:checkout_logs, "checkout_logs.returned_date IS NULL") } end # app/models/checkout_log.rb class CheckoutLog < ApplicationRecord belongs_to :book end
之后查询逻辑可以简化为更易维护的形式:
@book = Book .where(book_number: params[:book_number]) .available .first
这种写法生成的查询会使用LEFT JOIN判断关联记录是否存在,在数据量较大时性能表现优于NOT IN 子查询,也更符合Rails的常规开发习惯。
内容的提问来源于stack exchange,提问作者kmacrae

