ORM概念、实现和使用
1. ORM概述
ORM(Object-Relational Mapping,对象关系映射)是通过实例对象的语法,完成关系型数据库操作的技术。
其映射关系如下:
数据库的表(table) --> 类(class)
记录(record) --> 对象(object)
字段(field) --> 对象的属性(attribute)
SQL --> 方法(function)例如,从DBConnection.exec("select targetName from table where ...")封装一层变成DB.table:find({name=targetName})。
2. 为什么要介绍ORM
network_adapter组件广泛使用:network_adapter中使用了很多ORM相关功能,接口看上去简单但是实现较为复杂,需要了解其工作原理。
架构演进方向:network_adapter组件和compute组件功能相似,但compute组件使用
mc.orm较少,且compute组件最近的更新开始使用ORM机制,可能是架构演进的方向。同名对象差异:
mc.mdb中有一个object_manage,mc.orm下也有一个同名对象,需要了解这两个同名对象的联系和区别。
3. mc.orm的实现
ORM相关的工具来自于/mc/orm文件夹。
文件夹中定义有:object.lua、object_manage.lua、object_collection.lua、factory.lua、tasks.lua。
ORM实现需要几个步骤:首先需要有连接数据库的入口,即存在一个被封装的数据库实例;然后是创建数据模型,也就是Model,这些都在model.json里面配置。自动生成代码中的db.lua是数据库的实现。映射方法的实现需要在业务代码(src目录)中进行,一些数据模型的定义过程已经在自动生成代码中提供,在service.lua中。
3.1 /mc/mdb/object_manage.lua
启动时从hwdiscovery组件获取自发现结果。具体来说,它会在D-Bus上获取hwdiscovery组件的ObjectGroup接口的getObjects方法,获得自发现的所有对象信息。它提供了一个on_add_object注册函数,可以传入一个D-Bus对象和回调方法,然后object_manage就会自动触发D-Bus上的自发现,并且对每个对象调用注册的回调函数,同时将这些资源树对象纳入管理。
3.2 /mc/orm/tasks.lua
利用skynet实现了一个轻量化的协程封装,用于以较低的代价实现协程并发。
3.3 /mc/orm/factory.lua
提供了一个类型工厂,即两个表。给定一个类名,factory会去从db中找到同名的类定义,然后获取具体定义并加入。同时factory在mdb_object_manage的信号上注册了回调,因此能完成一些自发现对象的管理。
3.4 /mc/orm/mdb_object_manager.lua
mdb.object_manage中有on_add_object注册函数,这里的mdb_object_manage就调用了这个,让mdb.object_manage去取自发现对象,然后注册回调把获取到的对象通过触发信号广播给回调。所以这个类相当于把mdb.object_manage通过信号做了一层封装,同时还提供回调的缓存。在初始化时发现的一堆回调会先缓存,可能是为了避免执行太久阻塞了object_manage。
3.5 /mc/orm/object_manage.lua
这是更加上层的封装,本身依赖了orm.factory、mdb_object_manage、db,还有bus,各种资源都依赖了。存在的意义主要是进行这些资源初始化的控制,因为这些资源的初始化步骤可能存在顺序,例如factory信号的注册需要mdb_object_manage实例。耗时操作也应该单独抽出来,例如mdb_object_manage中缓存回调函数的调用。
3.6 /mc/orm/object_collection.lua
这是提供对象增删改查功能的一个对象集合,每个类型对应一个Collection对象,集中管理该类型的所有对象,可以进行查询和访问操作。实际使用中基本都靠它提供的功能来调用,而它本身封装了db中的一些查找命令。
3.7 /mc/orm/object.lua
封装了一层task.lua和object_collection.lua,并且提供了一些信号用来进行资源树对象的管理,建立object_collection和资源树的连接,通过注册信号的回调函数来处理数据增删改查对应的资源树操作。可以认为是ORM被开发者使用的入口,传入一个类名,会先去factory获取缓存的类实例,如果没有则创建。在这个过程中注册各种信号回调,例如资源树上对象的属性变化信号,数据库中的对象新增和删除回调等,保证ORM层能够动态地反应它封装的资源变化。毕竟封装只是改变访问方式,不应该影响访问到的内容。
4. 上层调用方式
mc.orm对外提供的调用入口非常小,这些文件中基本上只需要用到mc.orm.object。
以network_adapter中的使用为例,\device\class路径下的各类对象都定义了orm.object包装。
实际上,在自动生成的代码中,service.lua中会自动生成一堆的CreateXXXX(SystemId, ID, prop_setting_cb)方法定义。其中,SystemId和ID是用来拼装出资源树对象path的,而prop_setting_cb是需要手写属性设置回调的。我们需要手写这些类的create_mdb_object方法调用这个Create方法并且传入属性设置回调函数,来定义如何创建资源树对象。在这里也能定义一些类型私有的特殊方法,也就是将资源树的一类对象封装成类的地方。这部分一个文件就像是面向对象编程中的类定义,ORM封装数据,在这里定义对数据的操作。
常见调用方式:
local c_object = require("mc.orm.object")
local c_network_adapter = c_object('NetworkAdapter')
local obj = c_network_adapter.collection:find({
Type = type,
SystemID = system_id,
SlotNumber = slot_number
})常用方法:
find(cb_or_table):获取一个对象fetch(cb_or_table):获取满足条件的所有对象fold(cb, acc):传入一个条件函数和一个访问状态,相当于进行一次遍历操作
fold方法实现示例:
local function fold_objects(objects, cb, acc)
local exit_loop = false
for key, obj in pairs(objects) do
acc, exit_loop = cb(acc, obj, key)
if exit_loop then
break
end
end
return acc
end5. 设计思考
5.1 信号机制的使用
信号在ORM中,甚至是整个V3都使用非常广泛。好处是把事件和处理解耦,订阅方甚至可以不关注是谁触发了这个事件,只要关注某个事件发生了要做什么就行。但是代价是会出现大量的回调函数,结合Lua中创建闭包的便利性,处理不好的话这些回调函数中使用的upValue有可能难以被释放,生命周期管理需要谨慎。
5.2 ORM的设计目标
ORM的设计目标与为什么要有mdbctl类似。busctl功能强大,但是我们想要获取某一类的对象只能沿着资源树一层一层查看和寻找。如果每一类对象都能单独管理,可能会更加方便。同时屏蔽数据库的操作细节也能让业务代码更能保持面向对象的思想。