Rails отношение многие ко многим с расширением, генерирующим некорректный SQL - PullRequest
0 голосов
/ 25 августа 2018

У меня проблема с отношением «многие ко многим» с «расширением», порождающим неправильный SQL.

class OrderItem < ApplicationRecord
  belongs_to :buyer, class_name: :User
  belongs_to :order
  belongs_to :item, polymorphic: true
end

class User < ApplicationRecord
  has_many :order_items_bought,
    -> { joins(:order).where.not(orders: { state: :expired }).order(created_at: :desc) },
    foreign_key: :buyer_id,
    class_name: :OrderItem

  has_many :videos_bought,
    -> { joins(:orders).select('DISTINCT ON (videos.id) videos.*').reorder('videos.id DESC') },
    through: :order_items_bought,
    source: :item,
    source_type: :Video do
      def confirmed
        where(orders: { state: :confirmed })
      end
    end
end

user.videos_bought.confirmed генерирует этот SQL:

Video Load (47.0ms)  SELECT  DISTINCT ON (videos.id) videos.* FROM 
"videos" INNER JOIN "order_items" "order_items_videos_join" ON 
"order_items_videos_join"."item_id" = "videos"."id" AND 
"order_items_videos_join"."item_type" = $1 INNER JOIN 
"orders" ON "orders"."id" = "order_items_videos_join"."order_id" INNER JOIN 
"order_items" ON "videos"."id" = "order_items"."item_id" WHERE 
"order_items"."buyer_id" = $2 AND ("orders"."state" != $3) AND "order_items"."item_type" = $4 AND 
"orders"."state" = $5 ORDER BY videos.id DESC, "order_items"."created_at" DESC LIMIT $6

, который возвращает Video записей, которые объединены с заказами, которые НЕ имеют подтвержденного состояния. Я ожидаю, что все заказы будут подтверждены государством.

Если я использую сырой SQL, все работает нормально:

has_many :videos_bought,
  -> {
    joins('INNER JOIN orders ON orders.id = order_items.order_id')
      .select('DISTINCT ON (videos.id) videos.*')
      .reorder('videos.id DESC')
  },
  through: :order_items_bought,
  source: :item,
  source_type: :Video do
    def confirmed
      where(orders: { state: :confirmed })
    end
end

Теперь user.videos_bought.confirmed генерирует этот SQL:

Video Load (5.4ms)  SELECT  DISTINCT ON (videos.id) videos.* FROM 
"videos" INNER JOIN "order_items" ON 
"videos"."id" = "order_items"."item_id" INNER JOIN orders ON 
orders.id = order_items.order_id WHERE 
"order_items"."buyer_id" = $1 AND ("orders"."state" != $2) AND
"order_items"."item_type" = $3 AND "orders"."state" = $4 ORDER BY
videos.id DESC, "order_items"."created_at" DESC LIMIT $5

Что кажется более лаконичным, потому что избегает автоматически сгенерированного order_items_videos_join имени. Он также возвращает только заказы с подтвержденным состоянием.

Есть идеи, что происходит? Иногда ActiveRecord генерирует ошибочный SQL?

Использование рельсов 5.1.5. Обновление до последней версии не имеет значения.

Я надеюсь получить объяснение того, почему Rails генерирует строку order_items_videos_join в первом случае, но не во втором. Кроме того, почему второй запрос SQL дает неверные результаты. При необходимости я могу отредактировать вопрос с помощью большего количества кода и образцов данных.

1 Ответ

0 голосов
/ 27 августа 2018

ActiveRecord иногда не просто генерирует ошибочный SQL, но в нем есть небольшой нюанс, так что начинать с простого лучше всего, когда речь идет об определении отношений. Например, давайте переработаем ваши запросы, чтобы получить это DISTINCT ON. Я никогда не видел необходимости использовать это предложение SQL.

Перед объединением в логическую схему сильно настроенной ассоциации, давайте просто посмотрим, есть ли сначала простой способ запроса, а затем проверим, есть ли веские основания для превращения ваших запросов в ассоциации.

Похоже, у вас есть такая схема:

Пользователь

  • ID

Заказать

  • ID
  • состояние

OrderItem

  • ID
  • order_id
  • customer_id (есть ли причина для OrderItem, а не для Order?)
  • item_type (Видео)
  • item_id (videos.id)

Видео

  • ID

Пара лакомых кусочков

  • Нет необходимости создавать расширения ассоциаций для условий запроса, которые позволили бы создать идеальные области для модели. Смотри ниже.

Очень хороший запрос может выглядеть как

Video.joins(order_item: :order).
    where(order_items: {
       buyer_id: 123, 
       order: {
          state: 'confirmed'
       }).
    # The following was part of the initial logic but
    #   doesn't alter the query results.
    # where.not(order_items: {
    #    order: {state: 'expired'}
    # }).
    order('id desc')

Вот еще один способ:

class User < ActiveRecord::Base
  has_many :order_items, foreign_key: 'buyer_id'
  def videos_purchased
    Video.where(id: order_items.videos.confirmed.pluck(:id))
  end
end

class OrderItem < ActiveRecord::Base
  belongs_to :order, class_name: 'User', foreign_key: 'buyer_id'
  belongs_to :item, polymorphic: true
  scope :bought, -> {where.not(orders: {state: 'cancelled'})}
  scope :videos, -> {where(item_type: 'Video')}
end

class Video < ActiveRecord::Base
  has_many :order_items, ...

  scope :confirmed, -> {where(orders: {state: 'confirmed'})}
end

...
user = User.first()
user.videos_purchased

Синтаксис может быть немного странным, когда дело доходит до имен таблиц и атрибутов, но это должно быть началом.

Обратите внимание, что я изменил его с одного на два запроса. Я предлагаю работать с этим до тех пор, пока вы действительно не заметите, что у вас есть проблема с производительностью, и даже тогда вам может быть проще просто кэшировать запросы, чем пытаться оптимизировать сложную логику объединения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...