Rails 的项目的发展历程一般是先把各种功能都怼到一个项目代码下,随着业务逐渐增长,项目会变得越来越庞大,融合了各种功能于一身,这时如果项目的某一个小功能出问题,很有可能影响整个业务,于是开始逐渐拆分解耦系统。
我们的业务过去也进行了各种拆分的尝试,开始是把非常独立的功能模块直接拆分出一套独立的代码项目,但主项目里仍然功能交错。于是乎,又开始考虑把主项目某些很通用且独立的数据拆分成微服务,其他项目使用 RPC 或 HTTP API 来读写数据,这样可以逐渐的去中心化,各个项目之间减少依赖。
起初简单的考虑使用一套服务独立存储这些数据,并提供 Restful 的 HTTP API 或 RPC 协议的接口,其他项目都通过该接口完成对数据的存取
因为起初没有确定是使用 HTTP API 还是 RPC 来进行数据交互,所以找了一些类似 AR 的中间层,发现还是有些质量不错的
比如 这个基于 RPC 的类 AR 数据层,使用类似 Mongoid 的字段声明方式,约定好对应的 service 来实现具体的 CURD 操作,比较简单。
另外还发现一个人气很高的基于 Restful API 的 ORM,基本实现了 AR 提供的大部分 model 方法,包括数据缓存,脏数据检查,关联关系,回调,数据校验,调用服务的接口认证等等,可以比较小的代价把 model 层从本地数据库替换成接口调用,只需要对应的接口服务提供一套 Restful 的数据接口即可。
两者相对 AR 的功能也都够用,但是还是和 AR 有着同样的问题,抽象成和连接层耦合的太紧密,如果想把 API 调用改为 RPC,或是混合使用,替换起来还是要大费周折。期望是能够把抽象成和连接层分隔开,对接方式如果更新只需要更改连接层即可,另外因为业务需要,也没法完全替换到,只是在项目中小范围尝试,还是期望以较小的改动完成一次替换。所以借鉴这些比较不错的代码,开始自己写轮子(虽然最后因为种种没有上线,过程还是非常有意思)
替换 model 首先预期对接的是一套 Restful API 服务,提供的数据和从数据库读到的结构一致,这样以来,只需要实现一个数据的抽象层,然后对应一个连接层用来对服务数据做读取。
首先实现一个类似 Activerecord::Base
的基类,定义好抽象层的功能以及和连接层的对接",思路也是借鉴了 active_remote 的实现方式
class ActiveRemoteRecord
include Virtus.model # 模型字段的getter/setter
def self.find(id)
# 查找数据
new proxy_service.find(id)
end
def self.created(params)
# 创建数据
new proxy_service.find(id)
end
def update_attributes(params)
# 更新数据
proxy_service.update_attributes(self.id, params)
end
def destroy
# 删除数据
proxy_service.delete(id)
end
def self.where(params)
# 按条件过滤数据
new proxy_service.search(id)
end
def self.proxy_class
# 这里直接约定一个连接层service的类,model的CRUD全部调用约定类来和数据服务做交互
"#{self.to_s}ProxyService".constantize
end
def self.search(params)
# 因为model层的where方法在下面还有他用,这里声明一个search 方法和where区别一下
proxy_service.where(params).map {|data| new(data)}
end
private
def self.proxy_service
proxy_class.instance
end
def proxy_service
@proxy_service ||= self.class.proxy_service
end
end
定义好这样一个 model 的话,如果对接一个 user 数据,只需要声明类来继承基类即可
class User < ActiveRemoteRecord
...
end
然后在 services 中实现对应的 proxy 类
# app/services/user_proxy_service.rb
class UserProxyService
include Singleton
def self.create(params)
# request_create_user_api
end
def update_attributes(id, params)
# request_find_user_api
end
...
end
这样就基本完成了一个 model 到 API model 的替换,如果想要替换 API 改用 RPC 或其他形式的调用协议,修改 proxy 并保持返回值一致即可。
完成 model 的替换后,在业务逻辑中还掺杂了很多关系方法调用,也为了保持上层逻辑的兼容,遂开始研究实现关系声明
简单来讲像 has_many/belongs_to 等声明关系的方法,在 AR 里主要就是生成特定的查询语句去获取到对应的数据,换成 API 来讲也就是使用 /users/:id/orders 这样的方法获取到 users 的所有 orders,所以思路也比较简单,就来实现一个简单的 has_many:
class ActiveRemoteRecord
...
def has_many(has_many_klass, options={})
# 动态定义关系方法
define_method has_many_klass do
# 获取实例缓存的关系变量
values = instance_variable_get("@#{has_many_klass}")
if values.blank?
# 获取关系对象类
class_name = options[:class_name] || has_many_klass
klass = class_name.to_s.classify.constantize
search_params = {}
if options[:as]
# 实现 polymorphic
search_params[:"#{options[:as]}_id"] = self.id
search_params[:"#{options[:as]}_type"] = self.class.name
else
foreign_key = :"#{self.class.name.demodulize.underscore}_id"
search_params[foreign_key] = self.id
end
# 调用关系类的where方法
values = klass.where(search_params)
instance_variable_set("@#{has_many_klass}", values)
end
values
end
end
end
这样当 User 关联多个 Order 时,可以直接在 User 表中声明 has_many :orders
,在 Order 声明多态的关联关系时只需加上 as
参数即可。
同理,实现 has_one
, belongs_to
也都是类似的思路
在 AR 中可以这样无数次的调用查询方法 User.where(condition_1).where(condition_2).where(...)
,直到最后要使用 .first
.all
这样的方法时才会真正的去执行 SQL 获取数据,这也是 AR 中懒加载的套路,在一次调用后会返回一个 ActiveRecord::Relation
的实例,保存着查询的条件,再次调用实际上是再次调用这个实例的方法而不是 model 类的 where 方法,直到最后需要真正读取数据时去执行搜索。按照这个思路也来实现一版简单的链式调用:
首先来实现一个类似 ActiveRecord::Relation
的类
class RemoteRecordCondition
def initialize(klass)
@klass = klass
end
def criteria
# 初始化的搜索条件
@criteria ||= {:conditions => {}}
end
def where(args)
# 再次调用时合并搜索条件并返回当前对象
# 这里简单实现把多条筛选语句直接合并
criteria[:conditions].merge!(args)
self
end
def each(&block)
# 调用该方法时才真正的搜索数据
@klass.search(criteria[:conditions]).each(&block)
end
def first
# 同上
@klass.search(criteria[:conditions]).first
end
def method_missing(method_name, *arguments, &block)
# 如果调用了数据类的类方法,中断当前的查询并直接返回类方法的结果
if @klass.respond_to?(method_name)
ret = @klass.send method_name
ret = ret.where(criteria[:conditions]) if ret.instance_of?(self.class)
return ret
else
super
end
end
end
然后把数据类的 where 方法从直接调用查询改为返回 RemoteRecordCondition 的实例:
class ActiveRemoteRecord
...
def self.where(params)
RemoteRecordCondition.new(self).where(params)
end
...
end
从头到尾复刻了 AR 的一些基础功能,如 attributes getter/setter
, assocation
, 并且可以和 AR 的 model 混用,定义数据间的简单关联关系。另外把连接层剥离开,方便替换。不过改动也比较底层最终还是没用到线上,不过过程比较有意思,也发现了很多好用的轮子