SPOD (简单对象数据库)

2014年09月16日 13:16GMT+8

SPOD 建立在 SQLite 或 Mysql 上. 全称为 Simple Persistent Object Database. SPOD可以映射数据库表(db table)为类(Class). 即每个继承sys.db.Object的类都可以看成为一个数据库表.

因此需要简单地修改类字段或调用类方法即可修改数据库表. 大多数情况下, 你只需要提供一些基础的声明, 而不需要编写单独的 SQL 语句, SPOD 是基于宏的, 也就是说类到数据库表的转换是在编译时完成的,而不是运行时, 这样就不用担心性能的问题.

原英文: http://old.haxe.org/manual/spod

示例: https://github.com/ncannasse/hxWiki/tree/master/src/db

SPOD类

下边说明中 SPOD 用于表示继承了 sys.db.Object 的类, 而小写的 spod 则为其实例(即一行数据).

对应类型

sys.db.Types.hx 文件 中包含了对应的 SQL 类型所对应的 Haxe 类型:

package sys.db;							// 注释内容为对应的 SQL 数据类型

/** int with auto increment, SId将会被声明为主键(primary key),在insert之后将自动赋值,并且动自增长 **/
typedef SId = Null<Int>

/** int unsigned with auto increment **/
typedef SUId = Null<Int>

/** big int with auto increment **/
typedef SBigId = Null<Float>

typedef SInt = Null<Int>				// 经典 32 位有符号整形数字 SQL INT

typedef SUInt = Null<Int>				// 无符号整形 SQL UNSIGNEDINT

typedef SBigInt = Null<Float>			// 一个 64 位有符号的整数 SQL BIGINT

/** single precision float **/
typedef SSingle = Null<Float>			// 单精度浮点值 SQL FLOAT

/** double precision float **/
typedef SFloat = Null<Float>			// 一个双精度浮点值 (SQL DOUBLE)

/** use tinyint(1) to distinguish with int **/
typedef SBool = Null<Bool>				// 一个布尔值 SQL TINYINT(1)

/** same as varchar(n) **/
typedef SString<Const> = String			// 一个限制大小的字符串值 SQL VARCHAR(K)

/** date only, use SDateTime for date+time **/
typedef SDate = Date					// 日期,不包含时间的部分 SQL DATE

/** mysql DateTime **/
typedef SDateTime = Date				// 日期+时间, SQL DATETIME

/** mysql Timestamp **/
typedef STimeStamp = Date				// 32 位日期时间戳 SQL TIMESTAMP

/** TinyText (up to 255 bytes) **/
typedef STinyText = String				// 文本 最多 255 个字节 SQL TINYTEXT

/** Text (up to 64KB) **/
typedef SSmallText = String				// 文本 最多 65 KB. SQL TEXT

/** MediumText (up to 24MB) **/
typedef SText = String					// 文本 最多 24MB. SQL MediumText

/** Blob type (up to 64KB) **/
typedef SSmallBinary = haxe.io.Bytes	// 二进制文件类型 最多 64KB. SQL BLOB

/** LongBlob type (up to 4GB) **/
typedef SLongBinary = haxe.io.Bytes		// 二进制文件类型 最多 4GB. SQL LongBLOB

/** MediumBlob type (up to 24MB) **/
typedef SBinary = haxe.io.Bytes			// 二进制文件类型 最多 24MB. SQL MediumBlob

/** same as binary(n) **/
typedef SBytes<Const> = haxe.io.Bytes	// 一个固定大小的字节值 SQL BINARY(K)

/** one byte signed [-128...127] **/
typedef STinyInt = Null<Int>			// 单字节有符号整形 [-128...127] SQL TINYINT

/** two bytes signed [-32768...32767] **/
typedef SSmallInt = Null<Int>			// 双字节有符号整形 [-32768...32767] SQL SMALLINT

/** three bytes signed [-8388608...8388607] **/
typedef SMediumInt = Null<Int>			// 3字节有符号整形 SQL MediumInt

/** one byte [0...255] **/
typedef STinyUInt = Null<Int>			// 单字节无符号 SQL TinyUInt

/** two bytes [0...65535] **/
typedef SSmallUInt = Null<Int>			// 双字节无符号 SQL SmallUInt

/** three bytes [0...16777215] **/
typedef SMediumUInt = Null<Int>			// 3字节无符号 SQL MediumUInt

// extra, 额外, 下边的类型没有对应的 SQL 类型

/** specify that this field is nullable **/
typedef SNull<T> = Null<T>				// 指定 SQL 字段值 可为NULL. 例: SNull<SDate>

/** specify that the integer use custom encoding **/
typedef SEncoded = Null<Int>			// 指定整数使用自定义编码 TODO: 未知

/** haxe Serialized string **/
typedef SSerialized = String			// 被 haxe 序列化(Serialize) 的字符串

/** native neko serialized bytes **/
typedef SNekoSerialized = haxe.io.Bytes	//

/** a set of bitflags of different enum values **/
/** 一组不同的二进制位 enum 值, 类似于多项选译 **/
typedef SFlags<T:EnumValue> = Null<haxe.EnumFlags<T>>

/** same as [SFlags] but will adapt the storage size to the number of flags **/
/** 和 SFlags 一样, 但是将会自动调整到 用于存储 标志的数量 的大小 **/
typedef SSmallFlags<T:EnumValue> = SFlags<T>;

/** allow to store any value in serialized form **/
typedef SData<T> = Null<T>				// 允许存储能被haxe序列化(Serialize)的任意值

/** allow to store an enum value that does not have parameters as a simple int **/
typedef SEnum<E:EnumValue> = Null<E>	// 允许存储不具有参数, 简单的int类型的 enum 值

元标记

可以将 元标记(metadata) 添加到 SPOD 类上用于声明一些附加信息.

用于成员字段:

  • @:skip: 忽略此字段,表示这个字段不是数据库结构中的一部分

  • @:relation: 将此作为一种关系的字段声明 见下面的特定部分

用于SPOD类:

  • @:table("myTableName"): 表名在数据库中的名称(默认与类名相同)

  • @:id(field1,field2,...): 指定表的主键字段。例如下面的类并没有一个唯一的 id 与自动递增,但两个字段组成 联合主键

    @:id(uid,gid)
    class UserGroup extends sys.db.Object {
    	public var uid : SInt;
    	public var gid : SInt;
    }
    
  • @:index(field1,field2,...,[unique]): 声明指定的字段为索引(index)-并将按此顺序进行索引。如果最后一个字段是 unique 类型这意味着这是唯一性索引(unique index)(可以声明多个@:index, 每个字段值的组合可以只能出现一次)

  • @:skip: ???跳过自动创建 manager,

  • @:skipFields ???忽略当前类(非父类和子类)的所有字段

    注: 这上边二个估计是用于多重继承时, 如: Session -> SessionData -> db.Object, 这时中间那个类基本都会同时添加这二个.

构建表格

通过继承 sys.db.Object, 后续将这个类称为SPOD类, 这个类对应为一个数据库表, 而类实例则为表中的一行数据,

import sys.db.Types;

class User extends sys.db.Object {
    public var id : SId;
    public var name : SString<32>;
    public var birthday : SDate;
    public var phoneNumber : SNull<SText>;
}

连接数据库

连接数据库并初始化表格.

var cnx : sys.db.Connection;
if( !useMysql )
	cnx = sys.db.Sqlite.open("mydatabase_file.db");
else {
	cnx = sys.db.Mysql.connect({
		host : "localhost",
		port : 3306,
		database : "MyDatabase",
		user : "root",
		pass : "",
		socket : null
	});
}
sys.db.Manager.initialize();		// 初使化

sys.db.Manager.cnx = cnx;			// 将 cnx 赋值给静态字段 Manager.cnx

// 在数库上建立表格,如果不存在
if(!sys.db.TableCreate.exists(User.manager)){
	sys.db.TableCreate.create(User.manager);
}

有二个静态方法可用于spod的初使化及结束:

  • sys.db.Manager.initialize(): 用于初始化SPOD类, 因此请确保至少一次在使用SPOD类之前调用它。

  • sys.db.Manager.cleanup(): 将清理缓存.

    (will cleanup the temporary object cache. This can be done if you are using server module caching to free memory or after a rollback to make sure that we don’t use the cached object version)

注意区别: SPOD::manager用于SPOD(表格)操作, 而sys.db.Manager管理连接到数据库.

sys.db.Connection 实例具有如下方法: 源码参见 各实现平台包如: neko/_std/sys/SQLite.hx

  • request(sql:String):sys.db.ResultSet: 例: request(“select * from User”);

  • close():Void: 关闭数据库连接

  • escape(s:String):String: 转义单引号

  • quote(s:String):String: 转义单引号并用单引号括起来: "'"+s.split("'").join("''")+"'"

    如果你使用 request(sql), 不要忘记 使用这个方法,以防止一些SQL注入语句

    例: request("select * from User WHERE name LIKE " + quote(name))

  • addValue(s:StringBuf, v:Dynamic ):Void;:

  • lastInsertId():Int: 需要在insert之后才有效,和最后一个插入的自增id值一致

  • dbName():String: 数据库类型名称, 如: SQLite

  • startTransaction():Void: 启动事务, 之后便可以进行 commit/rollback 操作.

  • commit():Void 确认,

  • rollback():Void: 回滚,回滚到最后一次的commit()

表格操作

每个SPOD都具有一个静态字段manager,用于表格操作,例如在默认的情况下 User.manager 将有如下方法:

参数 cond 为查询条件, cond将由宏解析并支持多种格式, 参看后边的搜索查询

  • all(?lock:Bool):List<User>: 参数带?表示可以为 Null 值,即(lock:Null<Bool> = null)

  • count(cond:Dynamic):Int: 返回符合条件的数量, 如需

  • dbClass():Class<Dynamic>: dbClass() == User

  • dbInfos():sys.db.RecordInfos: 获得表格信息,如主键,索引,

  • delete(cond:Dynamic, ?options:{}):Void: 根据条件(cond)删除行数据

    • options 类型为{?orderBy:field, ?limit:[pos,len], ?forceIndex:[field]}, 对于forceIndex 似乎只支持 Mysql
  • dynamicSearch(x:{}, ?lock:Bool):List<User>:

  • forceUpdate(o:User, field:String):Void:

  • get(id:Int, ?lock:Bool):User: 返回单个 spod(即一行数据)

  • search(cond:Dynamic, ?options:{}, ?lock:Bool):List<User>:

  • select(cond:Dynamic, ?options:{}, ?lock:Bool):User:

  • unsafeCount(sql:String):Int: sql 为 SQL语句,只是返回值不一致, 例: unsafeCount(“select count(*) from User”);

    注: 所有 unsafe为前缀的方法最后都将调用 unsafeExecute 也就是 cnx.request(string), 只是对sql做了一些附加处理而已.

  • unsafeDelete(sql:String):Void:

  • unsafeGet(id:Int, ?lock:Bool):User:

  • unsafeGetId(o:User):Int:

  • unsafeGetWithKeys(keys:{}, ?lock:Bool):User:

  • unsafeObject(sql:String,lock:Bool):User:

  • unsafeObjects(sql:String,lock:Bool):List<User>:

当你需要实现一些特殊的表格操作时,可以自定义一个 manager, 由于 Manager 类是基于宏的, 因此定义一个 manager 并没有那么简单

class User extends sys.db.Object {
	//......
	public static var manager = MyManager<User>(User);
}

插入

如果需要插入新的 SPOD, 可以简单的执行以下操作:

var u = new User();
u.name = "Random156";
u.birthday = Date.now();
u.insert();

在 .insert() 完成之后, 自动增量的唯一性 ID 将被设置为 NULL,

获取

为了找到你的 SPOD 实例(SQL 表的一行数据), 可以通过使用 对象的唯一标识符(主键) 调用 manager.get 方法:

var u = User.manager.get(1);
if(u == null) throw "User #1 not found!";
trace(u.name);

如果主键具有多个值, 则可以使用下行:

var ug = UserGroup.manager.get({ uid : 1, gid : 2 });

更新及删除

当修改过 SPOD 实例的字段之后通过调用 .update() 将更改发送到数据库:

var u = User.manager.get(1);
if( u.phoneNumber == null ) u.phoneNumber = "+3360000000";
u.update();

使用 .delete() 来从数据库中删除

var u = User.manager.get(1);
if( u != null ) u.delete();

搜索查询

使用 manager.search 方法:

var minId = 10;
for( u in User.manager.search($id < minId) ) {
    trace(u);
}

重要 为了区分 SQL 字段和 Haxe 变量, 所有 SQL 字段均以 美元符号($)作前缀.

上边的搜索查询语句在编译时将自动变成:

unsafeSearch("SELECT * FROM User WHERE id < "+Manager.quoteInt(minId));

代码生成器还可以确保 SQL 注入是永远不可能的.

语法

搜索查询支持下面的语法:

  • 常量、 整数、 浮点数、 字符串、null、true和false

  • 所有操作符 +, -, *, /, %, |, &, ^, >>, <<, >>>

  • 一元运算符 !, -, ~

  • 比较运算符 ==, >=, <=, >, <, !=

  • 条件测试 &&, ||

  • 圆括号(parenthesizes)

  • 调用及字段访问(编译成 Haxe 表达式)

额外的语法

可以使用 匿名对象以匹配某些字段的确切值 (similar to previous SPOD but typed :)

User.manager.search({ id : 1, name : "Nicolas" })
// same as :
User.manager.search($id == 1 && $name == "Nicolas")
// same as :
User.manager.search($id == 1 && { name : "Nicolas" })

同样可以使用基于 haxe 变量的 if 条件表达式用于生成 不同的SQL(在 if 条件测试表达式中不能使用数据库字段). 个人注: 感觉好混乱啊

function listName( ?name : String ) {
    return User.manager.search($id < 10 && if( name == null ) true else $name == name);
}

SQL 操作

在搜索查询中,可以使用以下 SQL 全局函数:

  • $now():SDateTime , 返回当前日期 (SQL NOW())

  • $curDate():SDate , 返回当前日期 (SQL CURDATE())

  • $date(v:SDateTime):SDate 返回的日期部分不包含时间 (SQL DATE())

  • $seconds(v:Float):SInterval 返回日期间隔, 以秒为单位 (SQL INTERVAL v SECOND)

  • $minutes(v:Float):SInterval 返回日期间隔的 分钟值(SQL INTERVAL v MINUTE)

  • $hours(v:Float):SInterval 返回日期间隔的 小时值(SQL INTERVAL v HOUR)

  • $hours(v:Float):SInterval 返回日期间隔的 小时值(SQL INTERVAL v HOUR)

  • $days(v:Float):SInterval 返回日期间隔的 天值(SQL INTERVAL v DAY)

  • $months(v:Float):SInterval 返回日期间隔的 月值(SQL INTERVAL v MONTH)

  • $years(v:Float):SInterval 返回日期间隔的 年值(SQL INTERVAL v YEAR)

您可以在搜索查询中使用以下 SQL 运算符:

  • stringA.like(stringB) : 将使用 SQL LIKE 操作符来查找 如果 stringB 包含在 stringA

    例: $name.like("J%") 或 "filedname".like("J%"),

  • TODO: 在源码中除了 like 还有 has, 但是没有 has 的文档, has 看上去用于 SFlags(EnumFlags)字段

SQL IN

haxe 2.09, 可以使用 haxe 的 in 运算符并获得类似效果作为 SQL IN

User.manager.search($name in ["a","b","c"]);

可以将任何 Iterable 传递到到 in 运算符, 一个空的 Iterable 将会发出 false 语句(statement) 用来防止 SQL 错误当执行 in () 时,

搜索选项

在搜索查询之后, 你可以指定一些搜索选项:

User.manager.search(true, {orderBy : name, limit : 20 });

支持以下选项:

  • orderBy: 可以指定几个排序字段, 使用减号(-)操作符将表示为降序.

    例如: orderBy : [-name,id] 将生成 SQL ORDER BY name DESC, id

  • limit: 限定搜索返回结果集, 可以使用 haxe 变量和表达式, 例如: { limit : [pos,length] }

  • forceIndex: 强制使用此索引搜索。 例如: { forceIndex : [name,date] }

Select/Count/Delete

不同于 search, 你可以使用 manager.select 方法, 它将只返回 第一个结果对象:

var u = User.manager.select($name == "John");

使用 manager.count 方法对匹配的搜索查询进行 计数统计:

var n = User.manager.count($name.like("J%") && $phoneNumber != null);

删除给定的查询匹配的所有 行(或 SPOD 实例)

User.manager.delete($id > 1000);

关系

可以声明使用的数据库之间的关系通过 @:relation 元标记:

class User extends sys.db.Object {
    public var id : SId;
    // ....
}
class Group extends sys.db.Object {
	public var id : SId;
   // ...
}

@:id(gid,uid)
class UserGroup extends sys.db.Object {
    @:relation(uid) public var user : User;
    @:relation(gid) public var group : Group;
}

当第一次从 UserGroup实例 中读取 user 字段, SPOD 将获取 uid 所对应的User实例并将其缓存. 如果你更改了 user 字段,它将在同一时间修改 uid 值.(If you set the user field, it will modify the uid value as the same time.)

(个人注: 这是因为它们引用的是同一个值, 但是如果执行了 user.delete(), 是需要自已删除相关联的表格数据, 小心表格的级联处理)

锁定

当使用事务, 关系默认为没有锁定, 你能进行 行锁定 (SQL SELECT…FOR UPDATE)通过添加 lock 如下:

@:relation(uid,lock) public var user : User;

级联

在MySQL/InnoDB中通过使用CONSTRAINT/FOREIGN KEY关系可以被强制约束.这时当删除一个User实例时, 所有对应的UserGroup实例都将被删除, 但是如果关系字段(relation field)可为空(nullable), 那么对应的实例将设置为 NULL;

如果你想要可为空的强制约束类型的关系字段, 可以如下添加 cascade:

@:relation(uid,cascade) var user : Null<User>;

(个人SQLite测试: 即使从 User 里删除了一行数据,但对应的 UserGroup 里相应的 user 值却还存在,而且不是为空,除非在delete之后调用 cleanup(), )

关系搜索

You can search a given relation by using either the relation key or the relation property

var user = User.manager.get(1);
var groups = UserGroup.manager.search($uid == user.id);
// same as :
var groups = UserGroup.manager.search($user == user);

第二种情况更为严格, 因为它不只检查 key 具有相同的类型, 也更安全,因为如果 user 的值为 null 在运行时,它将使用null id。

动态搜索

如果你想要建立在运行时精确值的搜索条件, 可以使用 manager.dynamicSearch, 它将生成基于 SQL 值的查询:

var o = { name : "John", phoneNumber : "+818123456" };
var users = User.manager.dynamicSearch(o);

请注意, 如果 Object 的属性 不在数据库表的字段中, 将得到一个 运行时错误.

序列化数据

为了在 SPOD对象中存储任意数据, 可以使用 SData 类型, 例如:

import sys.db.Types
enum PhoneKind {
    AtHome;
    AtWork;
    Mobile;
}
class User extends sys.db.Object {
    public var id : SId;
    ...
    public var phones : SData<Array<{ kind : PhoneKind, number : String }>>;
}
  • 当访问读取 phones 字段时(只在第一次),它是已经反序列化的. 默认情况下 数据将存储为 haxe-serialized 字符串, 但是你可以自定义 Manager,创自定义的 序列及反序列化方法.

  • 当 phones 字段被读取或写入时, 将设置 标志以记住其变化

  • 当 SPOD 对象插入(insert)或更新(update)时, 修改后的数据将被序列化并最终发送到数据库,如果发生了更改.

因此, 压入数据到 phones 数据或直接修改 phones 号码将被 SPOD 引擎察觉到.

SDate 是二进制的 blob. 为了允许任何类型的序列化(文本或二进制), 由于 SPOD 的设置所以 phones 字段实际占用的字节, 只能通过反射(Reflect)访问

访问 SPOD 信息

通过调用 manager.dbInfos() 方法返回 数据库表结构, 它将返回一个 sys.db.SpodInfos 结构。

自动插入,搜索,编辑生成

dbadmin 项目提供了一个基于 HTML 的界面, 允许插入和搜索/编辑和删除 SPOD 对象根据已编译的 SPOD 信息, 它还允许数据库同步根据 SPOD 结构在编译时自动检测差异在编译时和当前 DB之间.

注意这个库只是个 haxelib, 并非独立的应用.