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

xeruzo · 2019年08月15日 · 最后由 zhengpd 回复于 2019年08月17日 · 463 次阅读

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的字段,这样就不用再特地去遍历一遍了

共收到 9 条回复

可以在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中处理

spike76 回复

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

hiveer 回复

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

xeruzo 回复

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

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

还是感谢你的思路!

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

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

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册