unity里的一个数据库LiteDB

背景

在unity中使用数据库时,
发现发布到webgl的话不能使用sql,报了一堆谷歌不到的错.
不过在提到Sqlite不能使用地方通常都提到了使用另外一个数据库.
LiteDB.

简介

一个使用C#来写的nosql数据库.
特点:

  1. 可以像sqlite一样将数据存储到一个文件中
  2. 官方做了一些对比,性能比sqlite至少不差
  3. 重要的是其改编版本 UltraLiteDB 可以在unity发布到webgl后使用
  4. 库文件比较小,不影响游戏体积

为什么换用UltraLiteDB

  1. LiteDB初始化时似乎需要一个本地的文件路径,
    如果准备了一个远程的路径,解析发现本地不存在,就会报错
  2. UltraLiteDB体积更加小
  3. UltraLiteDB已经同步到是LiteDB的4.0版本(最新5.0),功能足够
  4. 作者在github提供了dll下载

简单使用

这里使用 UltraLiteDB 作为例子.
不过很遗憾作者没有在Readme页面介绍全部的使用方法.

没有模型

类似MongoDB,LiteDB存储使用的BsonDocument类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using UltraLiteDB;

void DatabaseTest() {
/**
* 打开或创建数据库
* 在unity应用中则会放在根目录下(于Assets,Library,Packages等同级)
* 如果是在webgl,则暂时不知道放在了哪里
*/
var db = new UltraLiteDatabase("MyData.db")

/* 获取集合(相当于表) */
var col = db.GetCollection("savegames");

/* 准备数据 */
var character = new BsonDocument();
character["Name"] = "John Doe";
character["Equipment"] = new string[] { "sword", "gnome hat" };
character["Level"] = 1;
character["IsActive"] = true;

/**
* 插入数据,会自动生成id
* 默认的id是一个UUID
* 且id也被定义为 character["_id"]
*/
BsonValue id = col.Insert(character);

/* 更新 */
character["Name"] = "Joana Doe";
col.Update(character);

/* 查询 */
List<BsonDocument> allCharacters = new List<BsonDocument>(characters.FindAll());

/* 删除 */
col.Delete(10);

/* upsert,存在则更新,否则插入 */
col.Upsert(character);

/* 需要关闭DB以结束对文件的锁定状态 */
db.Dispose();
}

为了防止忘记使用Dispose,可以使用

1
2
3
4
5
6
void DatabaseTest() {
using (var db = new UltraLiteDatabase("MyData.db")) {
var col = db.GetCollection("savegames");
// ...
}
}

有模型

但我想大多数人还是需要借助模型的,不然不断使用双引号要疯.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Model, 也称为POCO
public class User {

[BsonId] // 可能相当于[BsonField("_id")]
public int id { get; set; }

[BsonField("name")] // 这样做的一个好处就是不需要使用反射了
public string name { get; set; e}
}

// Test
using (var db = new LiteDatabase("MyData.db", BsonMapper.Global)) {
// UltraLiteDB不知为何需要借助BsonMapper来搞,可能LiteDB5.0解决了这个问题
var users = db.GetCollection<User>("users");

var user1 = new User() { name = "张三" };
var user2 = new User() { name = "李四" };

// 插入,由于id是int类型,因此生成的id也是从1开始的整数,而不是uuid
users.Insert(new[] {
user1,
user2
});

// 更新
user2.name = "王五";
users.Update(user2);

// 读取
IEnumerable<User> resUsers = users.FindAll();

// 删除
users.Delete(1);
}

封装使用

如果想摆脱无处不在的using,也准便摆脱这个文件到底是打开了还是没有的疑问.
一种办法就是搞单例模式.每次使用同一个数据库读写对象.
而在适当的时机销毁这个实例(比如unity某个全局的gameobject的OnDestroy事件中).

首先搞一个单例模式的数据库

1
2
3
4
5
6
7
8
9
10
using UltraLiteDB;

static class DB {
private static UltraLiteDatabase _instance;

public static UltraLiteDatabase GetInstance() {
var connectionString = new ConnectionString("MyData.db") { };
return _instance ??= new UltraLiteDatabase(connectionString, BsonMapper.Global);
}
}

然后搞个三行变一行的Helper
原本三行的操作(获取DB,获取集合,做操作),改编成helper的一个方法.
由于目前的个人需要仅仅是读,因此把常用的功能改编一下

  1. FindAll
  2. FindById
  3. Find
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static class DataHelper {

public static IEnumerable<T> FindAll<T>(string tableName) {
UltraLiteDatabase db = DB.GetInstance();
UltraLiteCollection<T> collection = db.GetCollection<T>(tableName);
return collection.FindAll();
}

public static T FindById<T>(string tableName, int id) {
UltraLiteDatabase db = DB.GetInstance();
UltraLiteCollection<T> collection = db.GetCollection<T>(tableName);
return collection.FindById(id);
}

public static IEnumerable<T> Find<T>(string tableName, Query query) {
UltraLiteDatabase db = DB.GetInstance();
UltraLiteCollection<T> collection = db.GetCollection<T>(tableName);
return collection.Find(query);
}
}

由于封装因人而异,事实上如果不嫌写字符串比较多,现在已经可以结束了
比如使用 DataHelper.FindById<User>("users", 1); 来查询users表的第一条.
但如果还想继续写点,那么可以

  1. 为每个Model固定下来表名,泛型的具体值
  2. 准备一些常用的查询
  3. 留下自定义查询的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System.Collections.Generic;
using Model;
using UltraLiteDB;

class Users {
private static readonly string tableName = "users";

// 常用查询一: 全部
public static IEnumerable<User> FindAll() {
return DataHelper.FindAll<User>(tableName);
}

// 常用查询二: 根据id
public static User FindById(int id) {
return DataHelper.FindById<User>(tableName, id);
}

// 常用查询三: users表中的某个字段type非常关键,常常用来查
public static IEnumerable<Category> WhereType(string type) {
return DataHelper.Find<Category>(tableName, Query.EQ("type", type));
}

// 防备其他查询,可以让用户自定义query条件
public static IEnumerable<User> Find(Query query) {
return DataHelper.Find<User>(tableName, query);
}
}

到这里,使用起来就有点像 Eloquent

1
2
3
4
5
6
7
8
9
10
IEnumerable<User> allUsers = Users.FindAll();

User firstUser = Users.FindById(1);

IEnumerable<User> someTypeUsers = Users.WhereType("tall");

User specificUser = Uesr.Find(Query.And(
Query.EQ("age", 20),
Query.EQ("type", "smart"))
).FirstOrDefault();

备注

尽管这里的查询使用的是Query,
但似乎LiteDB官方项目中还可以使用Lambda表达式的方法来写.
如果以后知道了就记录一下.

参考

  1. LiteDB官方
  2. UltraLiteDB官方