背景
Cache起源于没有SQL的1970时代,当时各种高级计算机语言才刚刚诞生,其中M语言较为独特,它的诞生就是为了在没有操作系统的机器上,进行数据存储。别忘了,Unix在1971年才发布。M语言别具一格地采用了Global多维数组,统一了复杂的内存操作和文件读写,使之成为了1970年代数据库的事实标准,特别是在医疗行业。而后Intersystems在1978年接过M语言的旗帜,在M语言上添加了SQL兼容层和ObjectScript层,前者顺应了时代的潮流,后者不仅为M语言提供了强大的OOP和各种便捷的语法糖,还让数据能以对象形式进行访问,让数据和代码更加紧密。
本文将简述多维数组、SQL、对象这3种数据操作方式,提供实例代码片段,并在运行效率、开发效率、管理效率、实用性方面讨论它们的优缺点。
为方便讨论,以学校与学生为例。对每种操作方法,都列举3种典型的用例,分别为,访问某特定ID的学生(即数据库ID索引)、访问某特定studentID的学生(即遍历唯一索引)、和访问某学校的所有人(即遍历非唯一索引)。
现假设学生表/对象定义如下:
Class Student Extends %Persistent
{
Property schoolId AS %String;
Property studentId As %String;
Property name As %String;
Index IdxOnSchoolId ON schoolId ;
Index IdxOnStudentId ON studentId [Unique];
Storage Default
{
<Data name="StudentDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>schoolId</Value>
</Value>
<Value name="3">
<Value>studentId</Value>
</Value>
<Value name="4">
<Value>name</Value>
</Value>
</Data>
<DataLocation>^StudentD</DataLocation>
<DefaultData>StudentDefaultData</DefaultData>
<IdLocation>^StudentD</IdLocation>
<IndexLocation>^StudentI</IndexLocation>
<StreamLocation>^StudentS</StreamLocation>
<Type>%Library.CacheStorage</Type>
}
}
方法1 多维数组
- 例1. 访问某特定ID的学生
s id = 1 // 已知id
s student = ^StudentD(id)
s name = $LIST(student, 4)
w name
- 例2. 访问某特定studentID的学生
s studentId = 1 // 已知studentId
s id = $ORDER(^StudentI("IdxOnStudentId",studentId,""))
s student = ^StudentD(id)
s name = $LIST(student, 4)
w name
- 例3. 访问某学校的所有人
s schoolId = 1 // 已知schoolId
s id=""
for {
s id = $ORDER(^StudentI("IdxOnSchoolId",schoolId,id))
q:id=""
s student = ^StudentD(id)
s name = $LIST(student, 4)
w name
}
$ORDER
方法返回多维数组最末端下标的下一个值。用来遍历多维数组。
方法2 SQL
- 例1. 访问某特定ID的学生
s id = 1 // 已知id
&sql(SELECT name INTO :name from Student where id=:id)
w name
- 例2. 访问某特定studentID的学生
s studentId = 1 // 已知studentId
&sql(SELECT name INTO :name from Student where studentId=:studentId)
w name
- 例3. 访问某学校的所有人
s schoolId = 1 // 已知schoolId
s query="SELECT name from Student where schoolId=?"
s statement=##class(%SQL.Statement).%New()
s sc=statement.%Prepare(query)
s rset=statement.%Execute(schoolId)
while (rset.%Next()) {
s name = rset.%Get("name")
w name,!
}
&sql()
为嵌入式SQL语句,在INTO
子句中赋值给变量,适合单行查询。&sql()
也可以返回游标Cursor,以实现多多行查询,但效率比SQL.Statement
低,不推荐使用。SQL.Statement
类实现动态SQL语句,动态查询适合返回多行结果。
方法3 对象
- 例1. 访问某特定ID的学生
s id = 1 // 已知id
s student = ##class(Student).%OpenId(id)
s name = student.name
w name
- 例2. 访问某特定studentID的学生
s studentId = 1 // 已知studentId
s student = ##class(Student).IdxOnStudentIdOpen(studentId)
s name = student.name
w name
- 例3. 访问某学校的所有人
s schoolId = 1 // 已知schoolId
s id=""
for {
s id = $ORDER(^StudentI("IdxOnSchoolId",schoolId,id))
q:id=""
s student = ##class(Student).%OpenId(id)
s name = student.name
w name
}
%OpenId
方法通过ID查找并返回对象。IndexOpen
方法通过唯一索引值查找并返回对象。注意,非唯一索引没有类似方法。
讨论
- 多维数组
- 运行效率: 高。
- 可控程度高,只要有老练的程序员,有足够的加班时间,有足够的资金和时间,总能打磨出最好的效率。据说多维数组的效率是SQL的10倍。
- 面向过程编程,能够实现SQL难以实现的逻辑控制。
- 注意,事实上,未经优化的多维数组操作未必比SQL效率高。
- 开发效率: 低。
- 虽然对于简单的数据操作,利用多维数组也能快速实现。但是一旦数组结构、索引、下标达到一定数量级,直接对为数组操作是个噩梦。代码中将充斥数组名,索引名,下标等magic values。
- 直接操作数组太过底层,数据校验、初始值、空置、锁管理、事务等都需要人工编码。
- 管理效率:低。
- 值和索引必须同时维护,稍有不慎,容易造成索引损坏。
- 不同熟练度的程序员实现可能千差万别,对锁的使用、回调函数的调用等容易产生分歧,统一化难度大。
- 一旦数据定义发生变化,或者数据分布发生变化,需要调整或者调优,都需要较大人力投入。
- 数据提取、数据迁移、数据备份等日常操作,都须要程序员参与。
- 实用性:高
- 对临时数据、不需要考虑数据提取的数据,多维数组是很好的Key-Value数据库。
- 运行效率: 高。
- SQL
- 运行效率: 中。
- SQL解析和优化需要耗费额外时间。
- 适合批量处理。
- 不适合面向过程的逻辑。
- 可控程度低,如果不使用Frozen Plan,实际执行策略变化大,造成系统不稳定的假象。
- 但是经过调优后的SQL,可以实现较好的执行效率。
- 开发效率: 高。
- SQL提供隔离等级、事务、锁表等指令,简化了并发。
- SQL是声明式语言,简洁明了,可读性高,使程序员更关注结果,而不是遍历各种索引的过程。
- 管理效率:高。
- SQL提供了数据定义、数据查询、数据更新等的统一化。
- 数据提取、数据迁移、数据备份可以通过标准SQL客户端。
- 自适应性高,对存储的变化,例如变更索引,变更数据分布等,都能自动适应。
- Intersystems为SQL提供了额外的权限配置。
- 实用性:高
- 运行效率: 中。
- 对象
- 运行效率: 低。
- 不支持对索引的遍历,只能通过主索引和唯一索引访问单一对象。
- 开发效率: 中。
- 使对列的访问转化成了对对象属性的访问,使对外键的访问转化成了对外键对象的访问,代码的语义性强,可读性高。
- 管理效率:中。
- 在锁管理、值校验等方面统一化程度比多维数组高。
- 和多维数组一样,也无法提供标准客户端来访问数据。
- 实用性: 中
- 实际应用中,持久类除了单个对象内字段的校验逻辑,几乎不包含业务逻辑。一是因为持久类必须稳定,一旦编译,要尽量避免再次编译。二是因为实际项目中,业务逻辑在业务层中,数据是相互依存的,例如退费数据需要退费审核,而这样的逻辑,不可能在某个数据对象中存在,只能在数据层之上的业务层才合理。
- 运行效率: 低。
Do's & Don'ts
- 批量的读写操作多用SQL。
- 写操作应尽量用SQL或者对象。
- 多维数组应尽量只用于读操作。
- 多维数组的读、写操作应封装在方法中。
Nice work
讲述的很清楚,值得学习
深度好文