Rails: Преобразуйте сложный SQL-запрос в Arel или ActiveRecord

#sql #ruby-on-rails #arel

Вопрос:

В приложении Rails, над которым я работаю, я настроил область для модели автора информационного бюллетеня, которая использует сложный запрос, который я понял в необработанном 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 для лучшей согласованности с другими частями кодовой базы. Я не могу до конца разобраться во всех деталях того, как это произошло, особенно в том, что касается правильной настройки подзапроса.

ИЗМЕНИТЬ: Изменил запрос, чтобы очистить newsletter_id аргумент, согласно предложению @Eyeslandic.

ПРАВКА 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
 

Комментарии:

1. Пожалуйста, опубликуйте свой код модели

2. Начните шаг за шагом NewsletterAuthor.where(discardet_at: nil) и так далее. Попробуйте создать подзапрос самостоятельно, а затем добавить его в основной запрос. Вы доберетесь туда.

3. С другой стороны, на самом деле нет ничего плохого в использовании SQL в том виде, в каком вы сейчас. Возможно, вы просто захотите убедиться, что входные данные обработаны, Model.find_by_sql(["select * from table where id = :newsletter_id", newsletter_id: newsletter_id]) чтобы не вводить sql-инъекции.

4. Оба отличных момента, @Eyeslandic. Я изменил его, чтобы очистить входные данные, и попытаюсь разбить его на операторы.

5. Я добавил в модели, @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 
 

Комментарии:

1. Вау, @engineersmnky, это фантастика! Большое вам спасибо за то, что прошли через это. Мне действительно пришлось внести пару изменений в ваш AR-запрос, чтобы заставить его работать, но все это есть. Во-первых NewsletterStory , это пространство Newsletter::Story имен, и .exists предложение должно было передаваться по каналу через Arel ( .arel.exists ), чтобы соответствующим образом обернуть этот подзапрос в оператор EXISTS.

2. Кроме того, я действительно ценю простоту разбиения group_members запроса на отдельный запрос, а затем простого использования результатов where . Я думал об этом как об одном большом вложенном запросе с поиском члена группы в качестве подзапроса, но об этом гораздо проще рассуждать. Еще раз спасибо!

3. @MichaelBester рад, что это сработало для вас. Я принял правку, которую вы внесли, чтобы исправить эти недостатки.