# 背景 Cache起源于没有SQL的1970时代,当时各种高级计算机语言才刚刚诞生,其中M语言较为独特,它的诞生就是为了在没有操作系统的机器上,进行数据存储。别忘了,Unix在1971年才发布。M语言别具一格地采用了Global多维数组,统一了复杂的内存操作和文件读写,使之成为了1970年代数据库的事实标准,特别是在医疗行业。而后Intersystems在1978年接过M语言的旗帜,在M语言上添加了SQL兼容层和ObjectScript层,前者顺应了时代的潮流,后者不仅为M语言提供了强大的OOP和各种便捷的语法糖,还让数据能以对象形式进行访问,让数据和代码更加紧密。 本文将简述多维数组、SQL、对象这3种数据操作方式,提供实例代码片段,并在运行效率、开发效率、管理效率、实用性方面讨论它们的优缺点。 为方便讨论,以学校与学生为例。对每种操作方法,都列举3种典型的用例,分别为,访问某特定ID的学生(即数据库ID索引)、访问某特定studentID的学生(即遍历唯一索引)、和访问某学校的所有人(即遍历非唯一索引)。 现假设学生表/对象定义如下: ```java 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 { %%CLASSNAME schoolId studentId name ^StudentD StudentDefaultData ^StudentD ^StudentI ^StudentS %Library.CacheStorage } } ``` # 方法1 多维数组 * 例1. 访问某特定ID的学生 ```java s id = 1 // 已知id s student = ^StudentD(id) s name = $LIST(student, 4) w name ``` * 例2. 访问某特定studentID的学生 ```java s studentId = 1 // 已知studentId s id = $ORDER(^StudentI("IdxOnStudentId",studentId,"")) s student = ^StudentD(id) s name = $LIST(student, 4) w name ``` * 例3. 访问某学校的所有人 ```java 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的学生 ```java s id = 1 // 已知id &sql(SELECT name INTO :name from Student where id=:id) w name ``` * 例2. 访问某特定studentID的学生 ```java s studentId = 1 // 已知studentId &sql(SELECT name INTO :name from Student where studentId=:studentId) w name ``` * 例3. 访问某学校的所有人 ```java 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的学生 ```java s id = 1 // 已知id s student = ##class(Student).%OpenId(id) s name = student.name w name ``` * 例2. 访问某特定studentID的学生 ```java s studentId = 1 // 已知studentId s student = ##class(Student).IdxOnStudentIdOpen(studentId) s name = student.name w name ``` * 例3. 访问某学校的所有人 ```java 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或者对象。 - 多维数组应尽量只用于读操作。 - 多维数组的读、写操作应封装在方法中。