【问题标题】:Select simple value in ActiveRecord在 ActiveRecord 中选择简单值
【发布时间】:2016-11-27 18:10:37
【问题描述】:

我想在 Rails 应用程序中显示模型的文本摘要。

目前我正在这样做:

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(&amp;:description)
  5. 我知道这个查询只返回一个结果,但是它构建了一个包含所有结果的数组,所以我从数组中提取了一个结果
    • .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;

这是我正在生成的查询,问题是结果包含在模型对象中,然后我将其转换为数组,然后提取我想要的值。

那么,我如何告诉活动记录选择单个原始值而不是模型列表?

顺便说一句,在.map 之前添加.first 是行不通的,因为它在执行的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

【问题讨论】:

    标签: ruby-on-rails postgresql rails-activerecord


    【解决方案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
    

    您还提到不能在map 之前使用first,因为它引入了不受欢迎的订单;您可以尝试使用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,在该N+1 issue 中,您先进行一次查询以获取商品,然后再对每种产品进行另一次查询。如果您有一个页面为多个服务订单显示此文本,则您必须处理另一个级别;通常你必须在includes 中声明一大堆表,即使它们是在link_text 方法中声明的。

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

    如果你这样做,我认为你实际上不必在 link_text 本身中拥有 includes,但如果你从那里删除它并在任何其他情况下调用 link_text,你会得到又是N+1问题。

    原答案

    我对您的架构如何组合在一起感到有些困惑:service_ordersitems 是一对多关系还是多对多关系? productsitems 有什么关系?而且我没有足够的声誉来发表评论。

    一般来说,您可以使用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 了解如何解决该问题。

    【讨论】:

    • 嘿 Max,您的建议确实有效,但它错过了该项目没有关联产品的情况。我编辑了问题以阐明架构,并更新了 SQL 以说明没有产品的项目(它是与 coalesce 的左外连接)。
    • @ThiagoNegri 谢谢,我尝试通过新编辑解决coalesce 问题。这样效果更好吗?
    • 它就像一个魅力!您知道是否可以进行“条件包含”?例如,我只需要包含没有描述的项目的产品。我问这个是因为“服务订单”本身包含一个“描述”字段,所以“link_text”仅在服务订单没有描述时使用,因此对于该特定服务订单,不需要包含项目和产品。
    • @ThiagoNegri 我不这么认为,但这应该没那么重要。 includes 所做的只是提前获取服务订单、项目和产品,只需几个查询并将其加载到内存中,这样当您调用 link_text 时,它就不必为每个项目查询数据库(这可能会导致性能问题)。我不是数据库调优专家,但我不认为要求比你需要的更多的项目会对性能产生重大影响,除非比例相当大。 (在下一条评论中继续...)
    • (...接上一条评论)如果需要,您可以尝试将初始服务订单负载拆分为 ServiceOrder.some_query.where(description: nil)ServiceOrder.some_query.where.not(description: nil).includes(items: :product)。但我不确定这是否比只调用includes 更有效。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-24
    • 2015-02-07
    • 1970-01-01
    • 1970-01-01
    • 2010-12-03
    相关资源
    最近更新 更多