本篇来自去年在公司内部的分享的提纲,一直静静躺在我的网盘里。
模型在 Rails 已经被潜移默化的特指继承自 ActiveRecord 的类或者说是有实体表对应的模型类,而实际工作中,特别是在数据展示层,表单、页面上的数据渲染,并不一定完全的跟数据模型对应,所以实践中也会为表单建模,或者使用 reform 等 gem,这边是虚拟模型的最初用途——静态的为特定需求建立模型。
在复杂的信息系统中,存在定制字段的情况:如 金数据 这样的问卷系统;复杂门类的电商系统中为不同品类的商品定义不同的商品参数;在企业信息系统中,针对档案、工作流需定义字段。这些场景的共同点是:数据是有结构的、数据是有类型的、数据是有约束的。建模恰恰是解决这类问题的最佳手段,而难点在于设计一套动态的建模机制。
而动态是 Ruby 最擅长的,所以,我发明了虚拟模型模式来解决动态定义数据结构场景的问题。
我也相信,Ruby 和 Rails 在信息系统的开发上大有可为,从很多媒体的文章看,这个企业 SaaS 在国内仍是蓝海。希望这个技巧能够让基于 Rails 的系统在交付效率和功能性上拥有优势。
本篇只涉及原理的介绍。
反模式是一种试图解决问题的方法,但通常会同时引发别的问题。反模式虽以不同的形式被广泛实践,但这其中仍存在一定的共通性。
...
规则总有例外。在某些情况下,本来认为是反模式的设计却可能是合理的,或者说至少是所有的方案中最合理的。
—— 摘自《SQL 反模式》:引言
http://guides.rubyonrails.org/active_model_basics.html
本质是一组接口,定义了 Rails 的模型应具有的行为以及和视图、控制器的交互规范。 从 Rails 5 开始,越来越多的 ActiveRecord 的功能逐步移动到 ActiveModel 当中。
(个人观点)代码的设计的可改进空间很大,由于 ActiveRecord 和 Arel 强耦合以致部分移动到 AM 的模块在 AR 中又被较大规模的重写了一遍,没有继承或复用。
render @person
render
通过对象的 to_partial_path
方法来确定使用哪个视图文件进行渲染)ActiveModel::Type.lookup(:boolean).cast('0') #=> false
)实现了 ActiveModel 接口的 Ruby 对象,通常用来做 Presenter
、Form Object
https://github.com/makandra/active_type https://github.com/solnic/virtus
用于显式声明属性,类似 Mongoid 的 field
,需要指明属性的类型,可在赋值时自动进行类型转换。
在 ActiveRecord 中,属性的存储分为三个层次:
有修饰符(modifier)机制,部分(其实只有 pg
)数据库适配器会实现修饰符,如 pg
增加了 array
修饰符,使得属性可以变成强类型的数组字段!
也可以指定默认值,虽然可以使用 proc
却执行 proc
时不包含上下文,作用很受限。
可以为不同类型的商品的商品参数分别建模(表),然后通过 STI 区分不同类型的商品并增加相应的关联。
好处:
缺点:
text
类型的 spec
列serialize
约定的 dump
和 load
即序列化和反序列化方法serialize :spec, 对应的商品参数虚拟模型类
优点:
缺点:
可以考虑使用使用现代数据库的 NoSQL 数据类型存储(如 hstore
、json
)、使用 elastic 或者索引表
如何设计来支撑运营能够自由创建商品类型并且可以为商品类型定制商品参数?
大体上
细节上
同时兼顾
开源项目中的实现,可参考 Redmine。
CustomField(id: integer, name: string, field_format: string, ...)
CustomValue(id: integer, customized_type: string, customized_id: integer, custom_field_id: integer, value: text)
模型设计清晰易懂,但是表单的渲染、字段的验证、数据提交的处理都要实现,相关代码将近 2000 行。
User.validates :name, presence: true
的写法为 User
模型增加新的验证class
关键字定义的类(例如定义在 app/models
目录里的模型类)的生命周期是跟 APP 的生命周期一致的,故不可以直接操作他们Class.new
方法可以实现动态创建类型,其形参为基类,于是如下代码是可行的new_user_class = Class.new(User)
new_user_class.validates :name, presence: true
将得到一个匿名的 User
的子类,对其修改将不会污染 User
和传统做法一样,我们需要一个模型来记录运营输入的商品参数的字段定义,然后,我们将这些定义转换成虚拟模型类
model = Class.new(VirtualModel::Base)
custom_fields.each do |field|
model.attribute field.name, field.field_format
# model.validates
end
便可得到标准的 Rails 模型,渲染也很简单
<%= form_for @instance do |f| %>
<% custom_fields.each do |field| %>
<% case field.field_type %>
<% when "text" %>
<% f.text_field field.name %>
<%# ... %>
<% end %>
<% end %>
<% end %>
Class.new
生成的匿名类 name
方法返回值为 nil
,而 ActiveModel::Naming
即 model.model_name
依赖这个方法的返回值,为 nil
时,将会产生异常,所以需要在虚拟模型的基类上覆写这个方法
https://github.com/rails-engine/form_core
dummy 应用包含了:
https://github.com/jasl-lab/duck_record
Fork 自 ActiveRecord 5.0,外科手术式的剥离了 Arel,并引入了类似 Mongoid 的 embeds_one
、embeds_many
DSL 用于支持嵌套子模型,从 pg
中剥离了类型的 array
modifier,用于支撑多重选择