【问题标题】:How to perform a conditional where not exists in Rails如何执行Rails中不存在的条件
【发布时间】:2020-01-09 17:37:16
【问题描述】:

基本上,我想要完成的是获取所有未注册的用户——所有users 没有注册并且在当前学期会话中。每个学期都有许多课程,这些课程都有会话(因此所有连接)。

为了做到这一点,我尝试不使用(没有成功)和NOT EXISTS,它可以正常工作。我知道正确性,因为有一个规范来验证结果,我只是用 SQL BE 查询替换 FE“计算”——这显然快了 100 倍。

我有以下原始 SQL 查询给我带来正确的结果:

  SELECT users.* FROM users WHERE NOT EXISTS (
    SELECT enrollments.user_id FROM enrollments 
      LEFT OUTER JOIN sessions ON sessions.id = enrollments.session_id 
      LEFT OUTER JOIN courses ON courses.id = sessions.course_id 
      WHERE enrollments.user_id = users.id 
      AND sessions.term_id = 7
  )
  AND users.id IN (…);

我正在尝试使用 Rails 进行转换,但效果不佳。我第一次尝试使用NOT IN,但我发现它们可能不等同:

User.left_joins(enrollments: :session) # same result with includes
    .where(id: current_students)
    .references(:enrollments)
    .where.not(enrollments: { ces_session: open_enroll_term.session })
    .uniq

所以我继续尝试使用.exists.not

User.left_joins(enrollments: :session) # same result with includes
    .where(id: current_students)
    .references(:enrollments)
    .where(enrollments: { session: open_enroll_term.session }).exists.not

实际返回 Arel 对象。所以不仅我的查询可能不正确,而且现在我还需要一些 Arel 让它返回一个 ActiveRecord 对象。

有什么线索吗?谢谢!

【问题讨论】:

  • 我认为,如果您用简单的英语说明您想要通过查询获得什么,这会有所帮助。
  • 完成。谢谢你的建议。希望现在更容易掌握。
  • 如果不向.not发送任何参数,则不会触发查询。它的预期用途是.where.not(id: User.enrolled)。不幸的是,ActiveRecord 没有“NOT EXISTS”查询方法,这意味着您将不得不依赖原始 SQL 查询

标签: sql ruby-on-rails activerecord arel


【解决方案1】:

由于您没有展示您的模型,我将做出一些假设。在以下模型中,我将仅包含必需的属性。当然,你会拥有更多(例如,Course 可能会有 nameCourseSession 可能会有 time,等等)。

在这一切结束后,您将能够通过执行以下操作让所有学生注册到给定学期:

User.enrolled_for_term(term)

您将能够通过以下方式获得所有未注册给定学期的学生:

User.not_enrolled_for_term(term)

首先,我们假设:

  • 您有一个Term 模型(例如“2019 年秋季”或“2019 年冬季”)
  • 您有一个Course 模型(例如“Bio 100”或“Chem 100”)
  • 一个Term 可以有多个Courses(“2019 年秋季”可能会提供“Bio 100”和“Chem 100”)
  • Course 可以在许多Terms 中提供(“Bio 100”可能在“2019 年秋季”和“2019 年冬季”提供)
  • 您有一个CourseTerm 模型,它允许Terms 和Courses 之间的多对多关系

Term 可能看起来像:

# == Schema Information
#
# Table name: terms
#
#  id         :bigint           not null, primary key
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class Term < ApplicationRecord
  has_many :course_terms
  has_many :courses, through: :course_terms
end

Course 可能看起来像:

# == Schema Information
#
# Table name: courses
#
#  id         :bigint           not null, primary key
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class Course < ApplicationRecord
  has_many :course_terms
  has_many :terms, through: :course_terms
end

CourseTerm 可能看起来像:

# == Schema Information
#
# Table name: course_terms
#
#  id         :bigint           not null, primary key
#  course_id  :bigint
#  term_id    :bigint
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class CourseTerm < ApplicationRecord
  belongs_to :course
  belongs_to :term

  class << self

    def for_term(term)
      where(term: term)
    end

  end

end

请注意,您可以使用CourseTerm.for_term(term) 获取Term 的所有CourseTerms。

现在,我们假设 Course 可以在 Term 期间多次取消(例如,课程可能在上午 9:00 和上午 10:00 提供)。我认为这就是您所说的“会话”。我们称之为CourseSession,它可能看起来像:

# == Schema Information
#
# Table name: course_sessions
#
#  id             :bigint           not null, primary key
#  course_term_id :bigint
#  created_at     :datetime         not null
#  updated_at     :datetime         not null
#
class CourseSession < ApplicationRecord
  belongs_to :course_term

  class << self

    def for_term(term)
      where(course_term: CourseTerm.for_term(term))
    end

  end

end

请注意,您可以使用CourseSession.for_term(term) 获取Term 的所有CourseSessions。

现在,让我们假设 User 可以与 CourseSession 相关联,这就是我假设您所说的“注册”的意思。我称之为StudentCourseSession

# == Schema Information
#
# Table name: student_course_sessions
#
#  id                :bigint           not null, primary key
#  student_id        :integer
#  course_session_id :bigint
#  created_at        :datetime         not null
#  updated_at        :datetime         not null
#
class StudentCourseSession < ApplicationRecord
  belongs_to :course_session
  belongs_to :student, class_name: "User"

  class << self

    def for_course_sessions(course_sessions)
      where(course_session: course_sessions)
    end

    def for_term(term)
      for_course_sessions(CourseSession.for_term(term))
    end

  end

end

请注意,您可以通过 StudentCourseSession.for_term(term) 获取“学期”的所有注册。

现在,在“用户”模型中,添加 has_many :student_course_sessions 关联,指定 foreign_key: :student_id。您添加了一个enrolled_for_term(term) 方法,用于查找在指定学期注册的所有学生。然后,not_enrolled_for_term(term) 就变成了查找在指定学期注册的.not 的学生。

# == Schema Information
#
# Table name: users
#
#  id         :bigint           not null, primary key
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class User < ApplicationRecord
  has_many  :student_course_sessions, foreign_key: :student_id

  class << self 

    def not_enrolled_for_term(term)
      where.not(id: enrolled_for_term(term)).distinct
    end

    def enrolled_for_term(term)
      joins(:student_course_sessions).
        where(student_course_sessions: {id: StudentCourseSession.for_term(term)}).
        distinct
    end

  end

end

这是对上述内容的 rspec 测试:

require 'rails_helper'

RSpec.describe "Unenrolled Students" do
  before(:each) do 
    create(:student_course_session, student: user_1, course_session: course_session_1)
    create(:student_course_session, student: user_2, course_session: course_session_2)
    create(:student_course_session, student: user_2, course_session: course_session_3)
  end
  it "works" do 
    expect(User.enrolled_for_term(term_1)).to include(user_1)
    expect(User.enrolled_for_term(term_1)).to include(user_2)
    expect(User.enrolled_for_term(term_1)).not_to include(user_3)

    expect(User.not_enrolled_for_term(term_1)).not_to include(user_1)
    expect(User.not_enrolled_for_term(term_1)).not_to include(user_2)
    expect(User.not_enrolled_for_term(term_1)).to include(user_3)
  end
end

def user_1
  @user_1 ||= create(:user)
end

def user_2
  @user_2 ||= create(:user)
end

def user_3
  @user_3 ||= create(:user)
end

def term_1
  @term_1 ||= create(:term)
end

def course_1
  @course_1 ||= create(:course)
end

def course_2
  @course_2 ||= create(:course)
end

def course_term_1
  @course_term_1 ||= create(:course_term, course: course_1, term: term_1)
end

def course_term_2
  @course_term_2 ||= create(:course_term, course: course_1, term: term_1)
end

def course_term_3
  @course_term_3 ||= create(:course_term, course: course_2, term: term_1)
end

def course_session_1
  @course_session_1 ||= create(:course_session, course_term: course_term_1)
end

def course_session_2
  @course_session_2 ||= create(:course_session, course_term: course_term_2)
end

def course_session_3
  @course_session_3 ||= create(:course_session, course_term: course_term_3)
end

def student_course_session_1
  @student_course_session_1 ||= create(:student_course_session, student: user_1, course_session: course_session_1)
end

这给出了:

10:55:58 - INFO - Running: spec/so/unenrolled_students_spec.rb

Unenrolled Students
  works

Finished in 0.27546 seconds (files took 1.04 seconds to load)
1 example, 0 failures

【讨论】:

  • 感谢您的帮助。我确实有招生。所以学生报名参加一个会议。一个会话属于一个课程。当然可以在一个学期内进行一次会议。问题是我有一个学生名单。从这些学生中,我必须检查哪些学生没有在这个学期注册。注册查询很简单。也许我可以尝试按照您的提示并否定已注册的查询(尽管我将不得不修改我的,因为我还考虑了注册状态,例如未决,已完成,已撤回。我想这里的意思是,如果可以用where.not 与不存在 (SQL)。
  • 如果您使用上面的代码,并且有一个名为@students 的变量,那么只需执行@students.not_enrolled_for_term(term)。或者也许我错过了一个重要的微妙之处。
  • 感谢您的回复所以基本上否定了注册的术语。我认为它可能会起作用,只是担心这是一个 N+1,而我的 SQL 版本只是一个查询。
  • 这都是一个单一的查询。你知道怎么用explain吧?
  • 是的,我知道。让我测试一下,如果它有效,我会回到这里。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-13
  • 2023-01-17
  • 1970-01-01
相关资源
最近更新 更多