#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 рад, что это сработало для вас. Я принял правку, которую вы внесли, чтобы исправить эти недостатки.