Rails 1 对多的关系如何只显示 1 条记录

xeruzo · August 15, 2019 · Last by zhengpd replied at August 17, 2019 · 3448 hits

gem 'ransack'

# 用户
class User
   has_many :books
end
# 书本
class Book
  belongs_to :user
end

class UserController
  def index
    @users = User.left_join(:books).ransack({:books_id_in=>params[:book_ids]}).result
    render json: @users
  end
end

页面大概是显示一个表,有以下两列,需求点:书名那列要显示所有拥有的书名
用户名 所有书名
user.name user.books.map(&:name).join(',')

这样会触发 sql n+1 问题 经过查询,后来改成了

class UserController
  def index
    @users = User.left_join(:books).ransack({:books_id_in=>params[:book_ids]}).result
    @list = @users.preload(:books).map{|u| {username: u.name, book_names: u.books.map(&:name).join(',')} }
    render ....
  end
end

想请教一下除了上面这种,有其他优化的方法吗?

比如在查询的时候就直接把所有 has_many 的书的名字拼成了一个 sql 的字段,这样就不用再特地去遍历一遍了

可以在 User 表加个书名的字段,Book 表新建、删除、更新,去更新这个字段。

我觉得如果数据库的设计保持不变,那你的方案算是最优解。 或者按照@tmr的思路也可以

User.eager_load(:books).ransack({:books_id_in=>params[:book_ids]}).map { |u| {username: u.name, book_names: u.books.map(&:name).join(',')} }

试试嘞。 不过这种数据拼装我一般不会在 controller 中处理

Reply to spike76

这样写看上去在 map 的时候又变成 n+1 问题,因为 eager_load 实际效果同左连接

Reply to hiveer

加字段记录这个思路很好~ 感谢~ @tmr
但是,你说的是对的,因为实际业务上的 User 表字段已经不少了,不能再加了

Reply to xeruzo

不会有 n+1,每个 user 已经加载了 books,user.books 不会执行 sql

Reply to spike76

还真没有 n+1,不过发现另外一个问题 但是用 eager_load 会重新拿到所有字段,上面的 select 无效了 用 preload 是正常的(看 sql preload 的对象会单独查询,不会再 join 主表)

class UserController
  def index
    @users = User.left_join(:books).ransack({:books_id_in=>params[:book_ids]}).result.select(:name)
    # 这里map里面改了获取数据的方法,假设我不想暴露密码给到前端,只想展示我select的字段
    @list = @users.eager_load(:books).map{|u| u.attributes.merge({book_names: u.books.map(&:name).join(',')} }
    # 用eager_load  u.attributes 会拿到user的全部字段
    # 用preload 则不会,只拿到了select的字段(符合我的预期)
    render ....
  end
end

还是感谢你的思路!

Reply to xeruzo

eager_load 本来就是为了取代你第一行里的 left_joins 的。你这种写法,相当于 join 了两次,有点看不懂。 eager_load 自身的 select 语句会拼接到你指定的 select 语句后,所以会查出所有的字段

单从查询的角度,可以考虑试试 mysql 的 group_concat 函数:

user = User.left_joins(:books)
         .group('users.id')
         .select("users.*, GROUP_CONCAT(books.name) as book_names")
         .last
user.book_names

代码没有经过测试,只是提供一个思路方向

You need to Sign in before reply, if you don't have an account, please Sign up first.