Szhangbiao's blog

记录一些让自己可以回忆的东西

0%

Hive在Flutter Web上遇到的问题及解决方案

Hive 在我个人项目的使用上出现了问题,目前遇到两种错误:

  • 多次 open 同一Collection下的 Box,除第一个 Box 能正常使用外,其他的 Box 都不可用。
  • 使用BoxCollection来创建多个 Box,当使用Box put 数据时会报数据类型错误,即使我们提供了正确的TypeAdapter

经过一段时间的摸索后终于找到一条能正常使用的方法。

前言

为什么我们需要用 Hive 创建多个Box呢,因为业务上的需求我们要在 Web 端实现在关系型数据库上实现的效果,大致思路可以概括为:

  • 创建一个共享的 Collections 里面包含用户信息的 Box 和其他通用的 Box
  • 每当用户登录就用用户的 id 命名创建一个 Collections,它里面的多个 Box 是这个用户单独拥有的数据。

这样做的好处就是每个用户的数据是隔离的,不会都放在一个 Box 里造成数据量太多影响性能。

Hive 还是文件型数据存储方案,内存压力和 CPU 性能是绕不开的话题。所以,Hive 不适合存储过多的数据,Hive 的作者在issue中建议 1000 ~ 5000;超过这个值性能会逐渐降低。

错误的使用方式 1

使用 BoxCollection 的方案:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// hive/box/user_box.dart

import 'package:hive/hive.dart';

abstract class UserBox {
static String boxName = 'users';
Future<void> saveUser(User user);
Future<User?> findUserById(String userId);
void closeBox();
}

@Injectable(as: UserBox)
class UserBoxImpl extends UserBox {
CollectionBox<User>? _cachedBox; // 参考Hive框架的做法,打开一次后就把Box给缓存起来
final BoxCollection _collections;
UserBoxImpl(this._collections);

@override
Future<void> saveUser(User user) async {
CollectionBox<User> innerBox = await _getCachedBox();
await innerBox.put(user.id, user);
}

@override
Future<User?> findUserById(String userId) async {
CollectionBox<User> innerBox = await _getCachedBox();
return Future.value(innerBox.get(userId));
}

Future<CollectionBox<User>> _getCachedBox() async {
return _cachedBox ??= await _collections.openBox<User>(UserBox.boxName, collection: HiveManager.hiveBoxName);
}
}

// hive/box/person_box.dart

import 'package:hive/hive.dart';

abstract class PersonBox {
static String boxName = 'persons';
Future<void> savePerson(Person person);
Future<Person?> findPersonById(String personId);
void closeBox();
}

class PersonBoxImpl extends PersonBox {
LazyBox<Person>? _cachedBox; // 参考Hive框架的做法,打开一次后就把Box给缓存起来
final String userId;
final BoxCollection? _collections;
PersonBoxImpl(this.userId, this._collections);

@override
Future<void> savePerson(Person person) async {
LazyBox<Person>? innerBox = await _getCachedBox();
await innerBox?.put(person.id, person);
}

@override
Future<Person?> findPersonById(String personId) async {
LazyBox<Person>? innerBox = await _getCachedBox();
return Future.value(innerBox?.get(personId));
}

@override
void closeBox() {
_cachedBox?.close();
_cachedBox = null;
}

Future<Box<Person>?> _getCachedBox() async {
return _cachedBox ??= await _collections?.openBox<Person>(PersonBox.boxName, collection: userId);
}
}

// hive/hive_manager.dart

import 'package:hive/hive.dart';

class HiveManager {
static String hiveBoxName = 'hive-database';
// 提供一个静态方法来初始化,方便其他地方调用
static Future<HiveManager> initInstance() async {
_registerClassAdapter();
// init hive
await Hive.initFlutter();
BoxCollection collection = await BoxCollection.open(
hiveBoxName,
{
UserBox.boxName,
...
},
path: './',
);
UserBox userBox = UserBoxImpl(collection);
HiveManager hiveManager = HiveManager(userBox);
return Future.value(hiveManager);
}

static void _registerClassAdapter() {
Hive.registerAdapter<User?>(NullableUserAdapter());
Hive.registerAdapter<Person?>(NullablePersonAdapter());
}

final UserBox _userBox;
final PersonBox? _personBox;
const HiveManager(this._userBox,[this.personBox = null]);


UserBox get userBox => _userBox;
PersonBox? get personBox => _personBox;
// 用户登录成功后调用
Future<void> initBox(String userId) async {
BoxCollection collection = await BoxCollection.open(
userId,
{
PersonBox.boxName,
...
},
path: './',
);
personBox = PersonBoxImpl(userId, collection);
}
// 用户退出登录后调用
void closeBox() {
personBox?.closeBox();
}
}

这里的 CollectionBox,它内部持有一个 Box,从而可以对数据库进行数据处理。

错误的使用方式 2

在同一Collection下打开不同 Box 的方案:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// hive/box/user_box.dart

import 'package:hive/hive.dart';

abstract class UserBox {
static String boxName = 'users';
Future<void> saveUser(User user);
Future<User?> findUserById(String userId);
void closeBox();
}

@Injectable(as: UserBox)
class UserBoxImpl extends UserBox {
Box<User>? _cachedBox; // 参考Hive框架的做法,打开一次后就把Box给缓存起来

@override
Future<void> saveUser(User user) async {
Box<User> innerBox = await _getCachedBox();
await innerBox.put(user.id, user);
}

@override
Future<User?> findUserById(String userId) async {
Box<User> innerBox = await _getCachedBox();
return Future.value(innerBox.get(userId));
}

Future<Box<User>> _getCachedBox() async {
return _cachedBox ??= await Hive.openBox<User>(UserBox.boxName, collection: HiveManager.hiveBoxName);
}
}

// hive/box/config_box.dart

import 'package:hive/hive.dart';

abstract class ConfigBox {
static String boxName = 'configs';
Future<void> saveConfig(Config config);
Future<Config?> findConfigByKey(String configId);
void closeBox();
}

@Injectable(as: ConfigBox)
class ConfigBoxImpl extends ConfigBox {
Box<Config>? _cachedBox;
ConfigBoxImpl(this._localStorage);

@override
Future<void> saveConfig(Config config) async {
Box<Config> innerBox = await _getCachedBox();
await innerBox.put(config.id, config);
}

@override
Future<Config?> findConfigByKey(String configId) async {
Box<Config> innerBox = await _getCachedBox();
return Future.value(innerBox.get(configId));
}

Future<Box<Config>> _getCachedBox() async {
return _cachedBox ??= await Hive.openBox<Config>(ConfigBox.boxName, collection: HiveManager.hiveBoxName);
}
}

// hive/hive_manager.dart

import 'package:hive/hive.dart';

class HiveManager {
static String hiveBoxName = 'hive-database';
// 提供一个静态方法来初始化,方便其他地方调用
static Future<HiveManager> initInstance(UserBox userBox, ConfigBox configBox) async {
_registerClassAdapter();
// init hive
await Hive.initFlutter();
HiveManager hiveManager = HiveManager(userBox, configBox);
return Future.value(hiveManager);
}

static void _registerClassAdapter() {
Hive.registerAdapter<User?>(NullableUserAdapter());
Hive.registerAdapter<Config?>(NullableConfigAdapter());
}

final UserBox _userBox;
final ConfigBox _configBox;
const HiveManager(this.userBox,this._configBox);

UserBox get userBox => _userBox;
ConfigBox get configBox => _configBox;

void closeBox() {
// user box close
}
}

同一个Collection下的第二个第一次 Box 打开后并不会创建 database,导致后续数据库操作的方法无限等待。

正确的使用方式

其实能正常工作的方式也是上面两种方式的组合。
具体代码如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// hive/box/user_box.dart

import 'package:hive/hive.dart';

abstract class UserBox {
static String boxName = 'users';
Future<void> saveUser(User user);
Future<User?> findUserById(String userId);
void closeBox();
}

@Injectable(as: UserBox)
class UserBoxImpl extends UserBox {
Box<User>? _cachedBox; // 参考Hive框架的做法,打开一次后就把Box给缓存起来

@override
Future<void> saveUser(User user) async {
Box<User> innerBox = await _getCachedBox();
await innerBox.put(user.id, user);
}

@override
Future<User?> findUserById(String userId) async {
Box<User> innerBox = await _getCachedBox();
return Future.value(innerBox.get(userId));
}

Future<Box<User>> _getCachedBox() async {
return _cachedBox ??= await Hive.openBox<User>(UserBox.boxName, collection: HiveManager.hiveBoxName);
}
}

// hive/box/config_box.dart

import 'package:hive/hive.dart';

abstract class ConfigBox {
static String boxName = 'configs';
Future<void> saveConfig(Config config);
Future<Config?> findConfigByKey(String configId);
void closeBox();
}

@Injectable(as: ConfigBox)
class ConfigBoxImpl extends ConfigBox {
Box<Config>? _cachedBox;
ConfigBoxImpl(this._localStorage);

@override
Future<void> saveConfig(Config config) async {
Box<Config> innerBox = await _getCachedBox();
await innerBox.put(config.id, config);
}

@override
Future<Config?> findConfigByKey(String configId) async {
Box<Config> innerBox = await _getCachedBox();
return Future.value(innerBox.get(configId));
}

Future<Box<Config>> _getCachedBox() async {
return _cachedBox ??= await Hive.openBox<Config>(ConfigBox.boxName, collection: HiveManager.hiveBoxName);
}
}

// hive/hive_manager.dart

import 'package:hive/hive.dart';

class HiveManager {
static String hiveBoxName = 'hive-database';
// 提供一个静态方法来初始化,方便其他地方调用
static Future<HiveManager> initInstance(UserBox userBox, ConfigBox configBox) async {
_registerClassAdapter();
// init hive
await Hive.initFlutter();
await BoxCollection.open(
hiveBoxName,
{
UserBox.boxName,
ConfigBox.boxName,
...
},
path: './',
);
HiveManager hiveManager = HiveManager(userBox, configBox);
return Future.value(hiveManager);
}

static void _registerClassAdapter() {
Hive.registerAdapter<User?>(NullableUserAdapter());
Hive.registerAdapter<Config?>(NullableConfigAdapter());
}

final UserBox _userBox;
final ConfigBox _configBox;
const HiveManager(this.userBox,this._configBox);

UserBox get userBox => _userBox;
ConfigBox get configBox => _configBox;

// 用户登录成功后调用
Future<void> initBox(String userId) async {
...
}

void closeBox() {
// user box close
}

思路就是先用BoxCollection创建所有需要的 Box,然后以正常的方式打开对应的 Box,放弃官方提供的CollectionBox的方式。

总结

Hive 官方给的文档还是很好的,可能没想到有人会以这种方式来用吧。如果上面的方案不奏效我还会尝试其他方案。
其他的解决思路:

  • 使用 BoxCollection 时,是不是我们TypeAdapter里类型转换的方式不正确,比如尝试在自定义对象里用@HiveField注解,然后在write()read()方法对每个属性分别处理。
  • 放弃在同一个Collections里创建多个 Box 的方案,每个用户的数据都单独对应一个Collections,多个用户的数据存放到同一个 Box 中。