如何不使用STI从两个独立表创建ActiveRecord集合?
这确实是个很常见的棘手场景——两个业务属性类似但结构差异极大的模型,要做统一的列表展示又不想牺牲大数据量下的性能。我给你几个可行的方案,都能避免在内存里全量加载排序的问题:
方案1:数据库层面的联合查询(UNION ALL)
直接在数据库层面把两个表的查询结果合并,让数据库来处理排序和分页,这是性能最优的方式之一。因为两个表列交集小,我们可以手动指定需要的公共字段,用NULL填充差异字段,保证结果集结构一致。
class Company < ApplicationRecord has_many :customer_invoices, -> { order(due_date: :desc) } has_many :vendor_invoices, -> { order(due_date: :desc) } def invoices(sort_column: :due_date, sort_direction: :desc, page: 1, per_page: 20) # 定义两个查询的公共输出结构,差异字段用NULL填充 customer_query = customer_invoices.select( "id", "due_date", "amount", "'customer' AS invoice_type", "NULL AS vendor_reference" ) vendor_query = vendor_invoices.select( "id", "due_date", "amount", "'vendor' AS invoice_type", "vendor_reference" ) # 合并查询并做排序分页,最后转成易操作的结构化对象 combined = customer_query.union_all(vendor_query) .order("#{sort_column} #{sort_direction}") .page(page).per(per_page) combined.map do |row| OpenStruct.new( id: row.id, due_date: row.due_date, amount: row.amount, invoice_type: row.invoice_type, vendor_reference: row.vendor_reference ) end end end
优点:完全利用数据库的索引和优化能力,大数据量下性能稳定;
缺点:需要手动维护字段映射,后续表结构变更时要同步修改查询语句。
方案2:使用数据库视图(Database View)
把上面的联合查询逻辑固化成数据库视图,再创建一个只读模型对应这个视图,这样就能像操作普通ActiveRecord模型一样做分页排序,代码会更简洁。
首先在数据库中创建视图:
CREATE OR REPLACE VIEW combined_invoices AS SELECT id, due_date, amount, 'customer' AS invoice_type, NULL AS vendor_reference, company_id FROM customer_invoices UNION ALL SELECT id, due_date, amount, 'vendor' AS invoice_type, vendor_reference, company_id FROM vendor_invoices;
然后创建对应模型:
class CombinedInvoice < ApplicationRecord self.table_name = 'combined_invoices' belongs_to :company default_scope -> { order(due_date: :desc) } end
最后在Company模型中添加关联:
class Company < ApplicationRecord has_many :customer_invoices, -> { order(due_date: :desc) } has_many :vendor_invoices, -> { order(due_date: :desc) } has_many :combined_invoices end
控制器里就能像普通列表一样使用:
class InvoicesController < ApplicationController def index sort_col = params[:sort_column] || :due_date sort_dir = params[:sort_direction] || :desc @invoices = current_company.combined_invoices .order("#{sort_col} #{sort_dir}") .page(params[:page]) end end
优点:模型操作和普通ActiveRecord完全一致,代码简洁易维护;
缺点:视图需要手动维护,表结构变更时要同步更新视图;且视图是只读的,适合只做展示的场景。
方案3:分批次归并排序(适合排序字段单一的场景)
如果你的排序字段固定为due_date,可以分别对两个模型的查询结果做分页,再在应用层用归并排序的方式合并,只加载当前页及之前必要的数据,避免全量加载。
class Company < ApplicationRecord def invoices(page: 1, per_page: 20) per_page = per_page.to_i offset = (page - 1) * per_page total_needed = offset + per_page # 预加载足够的批次数据(只取到当前页需要的最大位置) customer_batch = customer_invoices.order(due_date: :desc).limit(total_needed).to_a vendor_batch = vendor_invoices.order(due_date: :desc).limit(total_needed).to_a # 归并两个有序数组,模拟数据库排序逻辑 merged = [] c_idx = 0 v_idx = 0 while merged.length < total_needed && (c_idx < customer_batch.length || v_idx < vendor_batch.length) if c_idx >= customer_batch.length merged << vendor_batch[v_idx] v_idx += 1 elsif v_idx >= vendor_batch.length merged << customer_batch[c_idx] c_idx += 1 elsif customer_batch[c_idx].due_date >= vendor_batch[v_idx].due_date merged << customer_batch[c_idx] c_idx += 1 else merged << vendor_batch[v_idx] v_idx += 1 end end # 截取当前页的数据 merged.slice(offset, per_page) || [] end end
优点:不需要修改数据库,利用已有的模型索引;
缺点:逻辑复杂,排序字段变更时要重写归并逻辑;当其中一个表数据量极大时,还是会加载较多数据到内存,但比全量加载好很多。
内容的提问来源于stack exchange,提问作者user225088
相关产品推荐
相关产品推荐

