2016-11-27 7 views
0

Я хочу показать сводку текста для модели в приложении Rails.Выберите простое значение в ActiveRecord

В настоящее время я делаю это так:

class ServiceOrder < ApplicationRecord 
    has_many :items, class_name: 'ServiceOrderItem', 
        dependent: :destroy, 
        inverse_of: :service_order 

    def link_text 
    items.left_outer_joins(:product) 
     .select("string_agg(coalesce(products.description, service_order_items.description), '; ') as description") 
     .group("service_order_items.service_order_id") 
     .map(&:description) 
     .first 
    end 
end 

class ServiceOrderItem < ApplicationRecord 
    belongs_to :service_order, inverse_of: :items 
    belongs_to :product, optional: true 
end 

class Product < ApplicationRecord 
end 

Что меня беспокоит то, что я пытаюсь выбрать одно значение, а не модель.

Этот запрос действительно возвращается "фальшивый" модель и извлечь значение, я хочу, но это своего рода Hacky:

  1. Добавить корректные отношения мне нужно
    • items.left_outer_joins(:product)
  2. Добавить значение выбора Я хочу
    • .select("string_agg(coalesce(products.description, service_order_items.description), '; ') as description")
  3. Добавить группу по п
    • .group("service_order_items.service_order_id")
  4. Выполнить запрос и извлечь описание "фальшивый" модель вернулась
    • .map(&:description)
  5. Я знаю, этот запрос возвращает только одно resu л, но он строит массив со всеми результатами, поэтому я извлечь один результат из массива
    • .first

Запрос Я хочу это:

select string_agg(coalesce(products.description, service_order_items.description), '; ') 
    from service_order_items 
    left outer join products on service_order_items.product_id = products.id 
    where service_order_items.service_order_id = :id 
    group by service_order_items.service_order_id; 

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

Итак, как я могу указать активную запись, чтобы выбрать одно необработанное значение, а не список моделей?

Кстати, добавление .first до .map не работает, поскольку оно включает заказ в выполненном SQL, который у меня не может быть (order by service_order_items.id).

Схема:

create_table "products", force: :cascade do |t| 
    t.integer "organization_id" 
    t.string "code" 
    t.string "description" 
    t.string "brand" 
    t.string "unit_of_measure" 
    t.datetime "created_at",  null: false 
    t.datetime "updated_at",  null: false 
    t.decimal "selling_price" 
    t.index ["organization_id"], name: "index_products_on_organization_id", using: :btree 
end 

create_table "service_order_items", force: :cascade do |t| 
    t.integer "service_order_id" 
    t.decimal "quantity" 
    t.string "description" 
    t.integer "product_id" 
    t.decimal "unit_price" 
    t.datetime "created_at",  null: false 
    t.datetime "updated_at",  null: false 
    t.index ["product_id"], name: "index_service_order_items_on_product_id", using: :btree 
    t.index ["service_order_id"], name: "index_service_order_items_on_service_order_id", using: :btree 
end 

create_table "service_orders", force: :cascade do |t| 
    t.integer "organization_id" 
    t.text  "description" 
    t.integer "state_id" 
    t.datetime "created_at",  null: false 
    t.datetime "updated_at",  null: false 
    t.integer "customer_id" 
    t.integer "sequential_id" 
    t.date  "start_date" 
    t.date  "end_date" 
    t.index ["customer_id"], name: "index_service_orders_on_customer_id", using: :btree 
    t.index ["organization_id"], name: "index_service_orders_on_organization_id", using: :btree 
    t.index ["state_id"], name: "index_service_orders_on_state_id", using: :btree 
end 

ответ

1

Новый ответ

Необходимость использовать описание на service_order_items, если есть не продукт, делает это немного сложнее.Если вы хотите сохранить свой собственный SQL, она должна быть возможность использовать pluck с тем же текстом, как ваш select (минус as description части):

def link_text 
    items.left_outer_joins(:product) 
    .group("service_order_items.service_order_id") 
    .pluck("string_agg(coalesce(products.description, service_order_items.description), '; ')") 
    .first 
end 

Вы также упомянули, что вы не можете использовать first перед тем map, потому что он ввел нежелательный порядок; вы можете попробовать использовать take вместо first, чтобы избежать этого, и в этом случае вам не понадобится pluck.

Обратите внимание, что в любом случае вы вводите некоторые зависимости от имен таблиц, что может вызвать проблемы с более сложными запросами, для которых требуются псевдонимы таблиц. Если вы хотите пойти на менее пользовательских SQL, самый прямой путь я могу думать о том, чтобы добавить следующий метод (возможно, с именем, которое лучше подходит для вашего приложения) к ServiceOrderItem:

def description_for_link_text 
    product.try(:description) || description 
end 

Тогда в ServiceOrder:

def link_text 
    items.includes(:product).map(&:description_for_link_text).join('; ') 
end 

includes(:product) следует избегать N+1 issue где вы делаете один запрос, чтобы получить детали, а затем еще один запрос для каждого продукта. Если у вас есть страница, отображающая этот текст для нескольких заказов на обслуживание, вам придется иметь дело с другим уровнем этого; часто вам нужно объявить целую кучу таблиц в includes, даже если они объявлены в методе link_text.

service_orders = ServiceOrder.some_query_or_scope.includes(items: :product) 
service_orders.each { |so| puts so.link_text } 

Если вы сделаете это, я не думаю, что вы на самом деле должны иметь includes в самой link_text, но если вы удалили его оттуда, и вы назвали link_text в любой другой ситуации, вы получите N +1 снова проблема.

Оригинальный ответ

Я немного смущен, как ваша схема совмещается: сделать service_orders и items имеют один-ко-многим или многие-ко-многим? Как products относится к items? И у меня недостаточно репутации, чтобы прокомментировать.

В общем, вы можете использовать pluck, чтобы получить массив значений только с теми атрибутами, которые вы хотите. Я не знаю, как это работает на виртуальных атрибутах, но вы можете определить отношения has_many :through, так что вам не нужно определять string_agg(products.description, '; ') as description, чтобы присоединиться к строкам вместе. То есть, если ваша ServiceOrder модель способна иметь products ассоциацию, как:

has_many :items 
has_many :products, through: :items 

тогда вы могли бы просто определить link_text, как products.pluck(:description).join("; "). Возможно, вам придется поиграть с вашим определением has_many :through, чтобы заставить его работать с вашей схемой. Кроме того, делать это таким образом означает, что вам нужно следить за потенциальными проблемами N + 1; см. Rails guide section on eager loading, как это сделать.

+0

Эй, Макс, ваше предложение действительно работало, но оно пропускает случай, когда элемент не имеет связанного с ним продукта. Я отредактировал вопрос, чтобы прояснить схему, а также обновил SQL для учета элементов без продуктов (это левое внешнее соединение с 'coalesce'). –

+0

@ThiagoNegri Спасибо, я попытался обратиться к проблеме 'coalesce' с новыми изменениями. Это работает лучше? – Max

+0

Он работал как шарм!Вы знаете, можно ли включить «условные включения»? Например, мне нужно включить продукты без описания. Я спрашиваю об этом, потому что сам «сервис-заказ» содержит поле «описание», поэтому «link_text» используется только тогда, когда у сервисного заказа нет описания, поэтому для этого конкретного заказа на обслуживание нет необходимости включать элементы и продукты. –

 Смежные вопросы

  • Нет связанных вопросов^_^