Rails:将复杂的 SQL 查询转换为 Arel 或 ActiveRecord

Posted

技术标签:

【中文标题】Rails:将复杂的 SQL 查询转换为 Arel 或 ActiveRecord【英文标题】:Rails: Convert a complex SQL query to Arel or ActiveRecord 【发布时间】:2021-11-27 23:02:26 【问题描述】:

在我正在开发的 Rails 应用程序中,我在 Newsletter Author 模型上设置了一个范围,该模型使用了我在原始 SQL 中找到的复杂查询,如下所示:

scope :without_submissions_for, lambda  |newsletter_id|
  query = <<~SQL.squish
    SELECT * FROM "newsletter_authors"
    WHERE "newsletter_authors"."discarded_at" IS NULL
    AND "newsletter_authors"."newsletter_id" = :newsletter_id
    AND "newsletter_authors"."group_member_id" IN (
      SELECT DISTINCT "group_members"."id" FROM "group_members"
      INNER JOIN "newsletter_stories" ON "newsletter_stories"."author_id" = "group_members"."id"
      WHERE "newsletter_stories"."author_id" = "group_members"."id"
      AND "newsletter_stories"."status" = 'draft'
      AND NOT (
        EXISTS (
          SELECT 1 FROM "newsletter_stories"
          WHERE "newsletter_stories"."author_id" = "group_members"."id"
          AND "newsletter_stories"."status" = 'submitted'
        )
      )
    );
  SQL
  find_by_sql([query, newsletter_id: newsletter_id])

这正是我需要的,连同一些上下文(下面的模型)是这样的:一个组有成员和时事通讯。其中一些成员可以是给定时事通讯的作者。这些作者可以为时事通讯撰写故事,每个故事都可以在草稿已提交(用于出版)、已发布已撤回 状态。草稿故事可能会或可能不会分配给特定的时事通讯,但所有其他州的故事都会分配给单个时事通讯。此查询可识别分配给特定时事通讯的作者,这些作者已撰写草稿但未向该时事通讯提交故事。

我很想知道如何将其转换为 Arel 或 Active Record 语句,以便与代码库中的其他地方更好地保持一致。我无法完全理解实现这一点的所有细节,尤其是在正确设置子查询方面。

编辑:根据@Eyeslandic 的建议更改了查询以清理newsletter_id 参数。

编辑 2: 以下是我在此处使用的模型,为清晰起见进行了精简。请记住,上面的范围是在Newsletter::Author 模型上:

group.rb

class Group < ApplicationRecord
  has_many :members, class_name: 'GroupMember'
  has_many :newsletters
end

group_member.rb

class GroupMember < ApplicationRecord
  belongs_to :group
  has_many :authorships, inverse_of: :group_member, class_name: "Newsletter::Author"
  has_many :stories, inverse_of: :author, class_name: "Newsletter::Story"
end

newsletter.rb

class Newsletter < ApplicationRecord
  has_many :authors, inverse_of: :newsletter
  has_many :stories
end

newsletter/author.rb

class Newsletter::Author < ApplicationRecord
  belongs_to :newsletter, inverse_of: :authors
  belongs_to :group_member, class_name: "GroupMember", inverse_of: :authorships
end

newsletter/story.rb

class Newsletter::Story < ApplicationRecord
  belongs_to :newsletter, inverse_of: :stories, optional: true
  belongs_to :author, inverse_of: :stories, class_name: "GroupMember"

  enum status: draft: "draft", submitted: "submitted", published: "published", _default: "draft"
end

【问题讨论】:

请发布您的型号代码 一步一步开始,NewsletterAuthor.where(discardet_at: nil)等等。尝试自行构建子查询,然后将其添加到主查询中。你会到达那里。 再说一次,像现在这样使用 SQL 并没有什么问题。您可能只想确保使用 Model.find_by_sql(["select * from table where id = :newsletter_id", newsletter_id: newsletter_id]) 对输入进行清理,以免引入 sql 注入。 两者都很好,@Eyeslandic。我已经对其进行了更改以清理输入,并将尝试逐句分解它。 我在模型中添加了@LyzardKyng。 【参考方案1】:

虽然我们当然可以在 Arel 中构建您的查询,但在稍微检查一下您的 SQL 之后,看起来使用 AR API 来构建它实际上会更简洁。

以下应该产生您正在寻找的确切查询(没有"newsletter_stories"."author_id" = "group_members"."id",因为连接已经暗示了这一点)

class Newsletter::Author < Application Record
  belongs_to :newsletter, inverse_of: :authors
  belongs_to :group_member, class_name: "GroupMember", inverse_of: :authorships
  scope :without_submissions_for, ->(newsletter_id) 
    group_members = GroupMember
         .select(:id)
         .joins(:stories)
         .where(newsletter_stories: status: 'draft')
         .where.not( 
           Newsletter::Story
            .select(1)
            .where(status: 'submitted')
            .where(Newsletter::Story.arel_table[:author_id].eq(GroupMember.arel_table[:id]))
            .arel.exists
         ).distinct
    where(discarded_at: nil, newsletter_id: newsletter_id, group_member_id: group_members)
  
end 

【讨论】:

哇,@engineersmnky,这太棒了!非常感谢您逐步完成。我确实必须对您的 AR 查询进行一些调整才能使其正常工作,但这一切都在那里。首先NewsletterStory 命名空间为Newsletter::Story.exists 子句必须通过 Arel (.arel.exists) 进行管道传输,以便在 EXISTS 语句中适当地包装该子查询。 另外,我真的很欣赏将group_members 查询分解为一个单独的查询,然后在一个简单的where 中使用结果的简单性。我认为它是一个大型嵌套查询,将组成员查找作为子查询,但这更容易推理。再次感谢! @MichaelBester 很高兴它为您工作。我接受了您为纠正这些缺陷所做的修改。

以上是关于Rails:将复杂的 SQL 查询转换为 Arel 或 ActiveRecord的主要内容,如果未能解决你的问题,请参考以下文章

ARel 模仿包含 find_by_sql

如何在 Arel 和 Rails 中进行 LIKE 查询?

Rails 3.0 中的 Arel 到底是啥?

Rails Arel查询,在现在之前获取列一定的持续时间

如何使用ARel对子查询进行连接?

Arel 中的嵌套查询