能否基于查询而非数据表或视图创建Rails只读模型
Rails 基于非物理表定义只读模型的方案说明
一、是否可以基于数据库视图定义Rails只读模型
完全可以。ActiveRecord 本身不强制要求模型必须绑定业务写入用的物理数据表,只要映射的数据源返回符合字段规范的结果集即可,这类只读模型广泛用于跨表聚合统计、多表关联结果扁平化查询等场景,只要不触发写操作就可以正常使用所有查询类方法。
二、是否必须提前在数据库中创建对应视图
不是必须,有两类成熟实现方案,可根据业务场景选择:
方案1:提前创建数据库物理视图
这是生产环境高频复杂查询场景的首选方案,实现步骤如下:- 编写数据库迁移创建视图,以PostgreSQL为例:
class CreateUserStatisticsView < ActiveRecord::Migration[7.1] def up execute <<~SQL CREATE VIEW user_statistics AS SELECT users.id AS user_id, users.nickname, COUNT(posts.id) AS post_count, COALESCE(SUM(posts.view_count), 0) AS total_views FROM users LEFT JOIN posts ON posts.user_id = users.id AND posts.published = true GROUP BY users.id, users.nickname SQL end def down execute "DROP VIEW IF EXISTS user_statistics" end end- 编写对应模型,指定表名、主键并强制只读:
class UserStatistic < ApplicationRecord self.table_name = "user_statistics" self.primary_key = "user_id" def readonly? true end end该方案的优势是查询逻辑下沉到数据库层,数据库可对视图做执行计划优化,查询性能更高,模型使用体验和普通物理表模型完全一致,支持scope定义、关联配置、条件过滤、排序等所有常规查询操作。
方案2:不创建物理视图,直接通过自定义查询语句实现同等效果
如果查询频次低、逻辑简单,完全可以不用建视图,直接通过ActiveRecord的from方法传入子查询即可,示例如下:class UserStatistic < ApplicationRecord self.abstract_class = true def self.base_list subquery = <<~SQL.squish ( SELECT users.id AS user_id, users.nickname, COUNT(posts.id) AS post_count, COALESCE(SUM(posts.view_count), 0) AS total_views FROM users LEFT JOIN posts ON posts.user_id = users.id AND posts.published = true GROUP BY users.id, users.nickname ) AS user_statistics SQL select(:user_id, :nickname, :post_count, :total_views).from(subquery) end end调用时直接链式追加查询条件即可,比如
UserStatistic.base_list.where(total_views: 50..).order(post_count: :desc),返回的结果集结构和物理视图方案完全一致,且天然不支持写操作,不会出现误改数据的问题。
三、选型注意事项
- 若查询逻辑复杂、调用频次高,优先选择物理视图方案,性能表现更稳定;若查询逻辑简单、仅在少数低频场景使用,直接用子查询方案即可,无需维护额外的数据库对象。
- 无论选择哪种方案,都建议明确指定结果集的唯一标识字段作为模型主键,避免使用
find、配置模型关联时出现主键匹配错误。 - 物理视图方案必须在模型层覆写
readonly?方法返回true,从应用层拦截误触发的写操作,避免抛出数据库层面的底层错误。
内容的提问来源于stack exchange,提问作者Ward Clark
相关产品推荐
相关产品推荐

