如何根据关联的完全匹配查找记录

How to find a record based on a full match of associations

我正在制作一个任务可以由用户完成的应用程序。这些任务具有关联 RequirementsUser 有关联 Qualifications

两者通过共享连接QualificationCategory

一个Task有很多要求,一个User有很多Qualifications.

需求的结构如下:

Requirement
- ID
- qualification_category_id
- task_type_id
- points_required

资格的结构如下:

Qualification:
- ID
- qualification_category_id
- user_id
- points

问题是这样的:

对于一个用户,我需要找到他被允许完成的所有任务。所以我想 select 数据库中 task_type.requirements 完全匹配 user.qualifications

的所有任务

因此,对于每个任务,根据 qualification_category 检查要求是否与任何用户资格相匹配,并且对于每个要求,检查相应的用户资格点数是否高于或等于所需点数。

型号

class Requirement < ApplicationRecord
  belongs_to :qualification_category
  belongs_to :task_type, inverse_of: :requirements
end

class Task < ApplicationRecord
  belongs_to :task_type
  belongs_to :data_sourceable, polymorphic: true, optional: true
  belongs_to :user_id, optional: true
  belongs_to :solution, class_name: 'Hypothesis', optional: true
  belongs_to :payment_period, optional: true

  has_many :worker_groups, through: :task_type
  has_many :requirements, through: :task_type
end

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :timeoutable, :confirmable, :lastseenable,
         :recoverable, :rememberable, :trackable, :validatable, :registerable

  has_and_belongs_to_many :roles
  has_and_belongs_to_many :worker_groups
  has_many :payment_periods
  has_many :qualifications, inverse_of: :user
  has_many :qualification_categories, through: :qualifications
end

class Qualification < ApplicationRecord
  belongs_to :qualification_category
  belongs_to :user, inverse_of: :qualifications
end

如何编写 MySQL 查询或 ARel 语句来完成此操作。该应用程序将包含数百万个任务,因此在 ruby 中执行此操作是不可取的。

db fiddle: https://www.db-fiddle.com/f/2jBx1gQhzqn1hYS5xD82n/0

对 return 所有 task 特定 user 有资格(基于匹配单个资格)的 MySQL 查询如下:

SELECT t.id
  FROM task t 
  JOIN task_type tt
    ON tt.id = t.task_type_id
  JOIN requirement r
    ON r.task_type_id = tt.id
  JOIN qualification q
    ON q.qualification_category_id = r.qualification_category_id
   AND q.points                   >= r.points_required
  JOIN user u
    ON u.id = q.user_id
 WHERE u.id = ?

这可能return"duplicates";所以我们可以添加一个 GROUP BY 子句,或者将连接操作更改为 EXISTS 谓词。


要仅匹配用户资格满足 所有 要求的任务,我们需要一些不同的东西:

SELECT t.id
  FROM task t
  JOIN task_type tt
    ON tt.id = t.task_type_id
 WHERE EXISTS
       ( SELECT 1
           FROM requirement r
           JOIN qualification q
             ON q.qualification_category_id = r.qualification_category_id
            AND q.points                   >= r.points_required
          WHERE r.task_type_id = tt.id
            AND q.user_id = ?                                 -- specific user
       )
   AND NOT EXISTS
       ( SELECT 1
           FROM requirement r
           LEFT
           JOIN qualification q
             ON q.qualification_category_id = r.qualification_category_id
            AND q.user_id = ?                                 -- specific user
          WHERE r.task_type_id = tt.id
            AND ( q.user_id IS NULL OR q.points < r.points_required )
       )

EXISTS 位检查用户是否具有至少一项符合要求的资格。

NOT EXISTS 位检查用户没有匹配 qualification.

的任务类别是否有任何 requirement

再考虑一下,我觉得EXISTS部分可以省略,我们只需要检查NOT EXISTS...有没有requirement不统计通过 qualification.

SELECT t.id
  FROM task t
  JOIN task_type tt
    ON tt.id = t.task_type_id
 WHERE NOT EXISTS
       ( SELECT 1
           FROM requirement r
           LEFT
           JOIN qualification q
             ON q.qualification_category_id = r.qualification_category_id
            AND q.user_id = ?                                 -- specific user
          WHERE r.task_type_id = tt.id
            AND ( q.user_id IS NULL OR q.points < r.points_required )
       )

(假设 qualification.pointsrequirement.points_required 不为空,因此不等式比较的计算结果为 TRUE。如果我们有可能出现 NULL 值,则需要适当地处理这些值。 )

为了补充 spencer 的回答,对于那些希望通过范围和 ARel 将其纳入其 rails 项目的人。

scope :qualified_for, ->(user) {
    task_types = TaskType.arel_table
    requirements = Requirement.arel_table
    qualifications = Qualification.arel_table
    requirements_conditions = requirements[:task_type_id].eq(task_types[:id])
                                  .and(
                                      qualifications[:user_id].eq(nil).or(qualifications[:points].lt(requirements[:points_required]))
                                  )

    requirement_query = Requirement.select(1).joins("LEFT
               JOIN qualifications
                 ON qualifications.qualification_category_id = requirements.qualification_category_id
                AND qualifications.user_id = #{user.id}").where(requirements_conditions).exists.not

    joins(:task_type).where(requirement_query)
  }