Rails 5 - как отсортировать данные на основе данных в другой таблице? - PullRequest
1 голос
/ 12 июля 2020

У меня есть эти модели:

class Product < ApplicationRecord
  has_many :product_documents
end
class ProductDocument < ApplicationRecord
  belongs_to :product
end

Я хочу перечислить ВСЕ продукты и отсортировать их по атрибуту product_documents.doc_type = 1 (примечание: для некоторых продуктов не загружено никаких документов, для некоторых есть N) .

Я пробовал что-то вроде этого:

@products = Product.joins(:product_documents)
                      .where('product_documents.doc_type = ?', 1)
                      .order('product_documents.doc_url')
                      .page(params[:page]).per(50)

Но этот запрос возвращает мне только те продукты, которые загрузили документ с doc_type=1.

Как мне перечислить все продукты (даже те, у которых нет документа с doc_type=1) и отсортируйте их по факту, если они загрузили документ с doc_type=1?

Ответы [ 3 ]

1 голос
/ 12 июля 2020

Вы используете where, но хотите sort. Используйте приведенный ниже код:

@products = Product.joins(:product_documents)
                      .order('product_documents.doc_type asc')
                      .order('product_documents.doc_url')
                      .page(params[:page]).per(50)

where используется для фильтрации данных. order используется для сортировки данных.

Приведенный выше код будет сортировать записи на основе doc_type и doc_url.

Если вы не хотите заказывать по doc_url , вы можете удалить этот код.

1 голос
/ 12 июля 2020

Несмотря на то, что все это можно сделать в одном запросе, станет практически невозможно добиться от него хорошей производительности, когда таблицы станут большими (именно поэтому вы разбиваете на страницы, верно?), Потому что это действительно два разных типа запросы.

Live Query

В Rails эти два запроса будут:

products_without_one = Product.where.not('EXISTS (?)',
  ProductDocument.where('products.id = product_documents.product_id').where(doc_type: 1).select(1)
)
products_with_one = Product.joins(:product_documents).where(product_documents: { doc_type: 1 }).distinct

Вы могли бы UNION результаты вместе, но большую часть времени вы будете листать только один из двух запросов, прежде чем начнете листать другой.

Если вы действительно хотите выполнить это в одном запросе, или ваши таблицы не станут действительно большими, тогда выполните:

SELECT *
FROM (
  SELECT DISTINCT ON(p.id) p.id, doc_type
  FROM products p
  LEFT JOIN product_documents pd ON pd.product_id = p.id
  ORDER BY p.id ASC, doc_type DESC
) sub
ORDER BY doc_type

Столбец кеша

Если вам нужно выполнять эти запросы часто и быстро, я предлагаю изменить запрашиваемые данные. Добавление нового столбца в Product, например:

class Product < ApplicationRecord
  has_one :primary_doc_one, class_name: ProductDocument.to_s
end

# making the query:
Product.order_by('primary_doc_one_id NULLS FIRST') # or LAST

или

# with a Product#num_doc_type_one column
Product.order_by(:num_doc_type_one)

Хотя вам нужно убедиться, что вы правильно поддерживаете эти столбцы с транзакциями и т. Д. (Избегайте обратных вызовов если можете).

Mis c

Я пропустил .order('product_documents.doc_url'), потому что это не имеет смысла. Если в Product много ProductDocument, какой doc_url следует использовать при сортировке? например, дано:

product_documents
id | product_id | doc_url
-------------------------
1  |      1     |   'A'
2  |      1     |   'C'
3  |      2     |   'B'

(игнорируя doc_type), если результат Product будет:

{ product_id: 1, doc_url: 'A' }
{ product_id: 2, doc_url: 'B' }

или

{ product_id: 2, doc_url: 'B' }
{ product_id: 1, doc_url: 'C' }
1 голос
/ 12 июля 2020

Это основная идея c, вы можете иметь оператор CASE в своем SQL предложении order by (по крайней мере, если вы используете Postgres). Если doc_type=1, то в заказах они сначала, все остальное идет после этого, тогда вы можете заказывать по другим столбцам.

Product.joins(:product_documents)
.order("
  CASE product_documents.doc_type
    WHEN 1 THEN 0 ELSE 1 END,
  product_documents.doc_url
")
...