如何获取同时以高性能方式"own"特定记录的所有记录



我在 Rails 应用程序中有以下两个模型

class Students < ApplicationRecord
has_many :courses
attr_accessor :name
end
class Courses < ApplicationRecord
has_many :students
attr_accessor :name, :course_id
end

我想以有效的方式获得与所选学生在同一班级的每个学生共享的所有课程的列表。

给定以下学生:

  • Jerry's courses ["math", "english", "spanish"]
  • Bob's courses ["math", "english", "french"]
  • Frank's courses ["math", "basketweaving"]
  • Harry's courses ["basketweaving"]

如果选择的学生是杰瑞,我希望返回以下对象

{ Bob: ["math", "english"], Frank: ["math"] }

我知道这将是一个昂贵的手术,但我的直觉告诉我,有一个比我正在做的事情更好的方法。这是我尝试过的:

# student with id:1 is selected student
courses = Student.find(1).courses
students_with_shared_classes = {}
courses.each do |course|
students_in_course = Course.find(course.id).students
students_in_course.each do |s|
if students_with_shared_classes.key?(s.name)
students_with_shared_classes[s.name].append(course.name)
else
students_with_shared_classes[s.name] = [course.name]
end
end
end

对于这种情况,是否有任何ActiveRecord或SQL技巧?

我想你想做这样的事情:

student_id = 1
courses = Student.find(student_id).courses
other_students = Student
.join(:courses)
.eager_load(:courses)
.where(courses: courses)
.not.where(id: student_id)

这将提供仅使用两个数据库查询courses的其他学生的集合,然后您需要缩小到您尝试创建的集合:

course_names = courses.map(&:name)
other_students.each_with_object({}) do |other_student, collection|
course_names = other_student.courses.map(&:name)
collection[other_student.name] = course_names.select { |course_name| course_names.include?(course_name) }
end

以上内容将构建collection其中键是学生姓名,值是与student_id所学课程相匹配的课程数组。

如果您设置了一个连接模型(根据需要,除非您使用has_and_belongs_to_many(,您可以直接查询它并使用数组聚合:

# given the student 'jerry'
enrollments = Enrollment.joins(:course)
.joins(:student)
.select(
'students.name AS student_name',
'array_agg(courses.name) AS course_names'
)
.where(course_id: jerry.courses)
.where.not(student_id: jerry.id)
.group(:student_id)

array_agg是Postgres特有的功能。在MySQL和我相信Oracle上,你可以使用JSON_ARRAYAGG来实现相同的目的。SQLite 只有返回逗号分隔字符串的group_concat

如果你想从那里得到一个哈希,你可以做:

enrollments.each_with_object({}) do |e, hash|
hash[e.student_name] = e.course_names
end

此选项不像 Gavin Millers 出色的答案那样独立于数据库,而是在数据库端完成所有工作,这样您就不必遍历 ruby 中的记录并整理它们没有共同点的课程。

最新更新