[官文翻译]Dart轻量超快键值数据库Hive - 自定义对象

2,267 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第18天,点击查看活动详情


原文链接:

pub: hive | Dart Package (flutter-io.cn)

pub译文: [译]纯Dart键值(对象)数据库hive - 掘金 (juejin.cn)

译时版本: hive 2.1.0


TypeAdapter(类型适配器)

Hive 支持所有的基本类型、 List 、 Map 、 DateTime 和 Uint8List 。如果想要存储其它对象,需要注册一个 TypeAdapter (类型适配器)用于将对象转换为二进制形式。

可以自己写一个 TypeAdapter 或者生成它。大多数情况,生成的适配器实际上表现很好。用手动写的适配器,有时候可以做一些改进。

注册适配器

当想要 Hive 使用 TypeAdapter 时,需要注册它。 需要做两件事情:适配器的实例和一个 typeId 。每个类型都有唯一的 typeId ,当从磁盘取到一个值时,它用来查找正确的适配器。所有的 typeId 允许在 0 到 223 之间。

Hive.registerAdapter(MyObjectAdapter());Copy to clipboardErrorCopied

确保固定地使用 typeId 。改动必须和之前版本的 box 兼容。

建议在打开任意 box 之前注册所有的 TypeAdapter

import 'package:hive/hive.dart';

class User {
  String name;

  User(this.name);

  @override
  String toString() => name; // 只用于打印
}

void main() async {
  // 注册适配器
  Hive.registerAdapter(UserAdapter()); 

  var box = await Hive.openBox<User>('userBox');

  box.put('david', User('David'));
  box.put('sandy', User('Sandy'));

  print(box.values);
}

// 可自动生成
class UserAdapter extends TypeAdapter<User> {
  @override
  final typeId = 0;

  @override
  User read(BinaryReader reader) {
    return User(reader.read());
  }

  @override
  void write(BinaryWriter writer, User obj) {
    writer.write(obj.name);
  }
}

生成适配器

hive_generator 包几乎可以为任何类自动生成 TypeAdapter

  1. 要为一个类生成 TypeAdapter ,可添加 @HiveType 注解并提供一个 typeId (0 到 223之间)
  2. 用 @HiveField 注解所有要存储的字段
  3. 运行编译任务 flutter packages pub run build_runner build
  4. 注册生成的适配器

示例

这里有一个 person.dart 库,使用带有唯一的 typeId 的 @HiveType 来注解 Person 类:

import 'package:hive/hive.dart';

part 'person.g.dart';

@HiveType(typeId: 1)
class Person {
  @HiveField(0)
  String name;

  @HiveField(1)
  int age;

  @HiveField(2)
  List<Person> friends;
}

正如所看到的,每个 @HiveField 注解的字段都有唯一的编号(每个类中唯一)。这些字段的编号用于标识 Hive 二进制中的字段,并且一旦类被使用,该编号就不应该被修改。

字段编号的范围可为 0~255

上面的代码生成的适配器类名为 PersonAdapter 。也可以使用 @HiveType 的可选参数 adapterName 改变适配器的类名。

更新一个类

如果一个存在的类需要修改 - 例如,类需要一个新的字段 - 但是你仍然希望读取旧适配器写入的对象,不必担心!不破坏现有代码更新生成的适配器很简单。只需要记住下面的规则:

  • 不要改变任何现有字段的字段编号。
  • 如果添加新的字段,任何使用『旧』适配器写入的对象仍然可用新适配器读取。这些字段只是会被忽略。类似地,新代码写入的对象可以被旧代码读取:解析时新的字段会被忽略。
  • 只要字段编号保持不变,字段可以重命名,甚至可以从 public 改为 private 或者反之。
  • 字段可以被移除,只要字段编号在更新的类中不再被使用。
  • 不支持改变字段的类型。应该创建一个新字段来代替。
  • 空安全可用之后,对于新的非空字段需要提供 defaultValue (默认值)。

枚举

为枚举生成适配器的机制几乎与为类生成适配器的机制相同:

@HiveType(typeId: 2)
enum HairColor {
  @HiveField(0)
  brown,

  @HiveField(1)
  blond,

  @HiveField(2)
  black,
}

上面的(更新)规则同样适用于更新枚举。

默认值

可以通过 @HiveField 注解的 defaultValue 参数为属性和字段提供默认值。

@HiveType(typeId: 2)
class Customer {
  @HiveField(1, defaultValue: 0.0)
  double balance;
}

用于自定义类型的默认值是在 hive: 2.0.4 和 hive_generator: 1.1.0 之后引入的。

也可以通过将 defaultValue 设置为 true 为枚举类型提供默认值。 如果没有为枚举类型设定默认值,第一个值会被用作默认值。

@HiveType(typeId: 2)
enum HairColor {
  @HiveField(0)
  brown,

  @HiveField(1)
  blond,

  @HiveField(2, defaultValue: true)
  black,
}

HiveObject

当在 Hive 中存储自定义对象时,可以继承 HiveObject 来轻松地管理对象。 HiveObject 提供了对象的键和有用的辅助方法,如 save() 或 delete()

这里有一个如何使用 HiveObject 的示例:

import 'package:hive/hive.dart';

void main() async {
  Hive.registerAdapter(PersonAdapter());
  var persons = await Hive.openBox('persons');

  var person = Person()
    ..name = 'Lisa';

  persons.add(person); // 第一次会存储该对象

  print('Number of persons: ${persons.length}');
  print("Lisa's first key: ${person.key}");

  person.name = 'Lucas';
  person.save(); // 更新对象

  person.delete(); // 从 Hive 中移除对象
  print('Number of persons: ${persons.length}');

  persons.put('someKey', person);
  print("Lisa's second key: ${person.key}");
}

@HiveType()
class Person extends HiveObject {
  @HiveField(0)
  String name;
}

class PersonAdapter extends TypeAdapter<Person> {
  @override
  final typeId = 0;

  @override
  Person read(BinaryReader reader) {
    return Person()..name = reader.read();
  }

  @override
  void write(BinaryWriter writer, Person obj) {
    writer.write(obj.name);
  }
}

如果想要使用查询,也需要继承 HiveObject 。


关系

有时,模型会相互关连。下面的 Person 类有一个名为 friends 的其它 Person 的列表。 也可以有其它的对象如 pets

class Person extends HiveObject {
  String name;
  
  int age;
  
  List<Person> friends;
  
  Person(this.name, this.age);
}

可以用一般的列表来存储这些 Person ,但是更新单个 Person 会相当复杂,因为 Person 对象会是冗余存储。

HiveList

HiveList 提供了解决上述问题的方法。它们允许存储实际对象的 "链接":

import 'package:hive/hive.dart';

void main() async {
  Hive.registerAdapter(PersonAdapter());
  var persons = await Hive.openBox<Person>('personsWithLists');
  persons.clear();
  
  var mario = Person('Mario');
  var luna = Person('Luna');
  var alex = Person('Alex');
  persons.addAll([mario, luna, alex]);
  
  mario.friends = HiveList(persons); // 创建一个 HiveList
  mario.friends.addAll([luna, alex]); // 更新 Mario 的 friends
  mario.save(); // 将改动持久化
  print(mario.friends);
  
  luna.delete(); // 从 Hive 中移除 Luna
  print(mario.friends); // HiveList 会自动更新
}

@HiveType()
class Person extends HiveObject {
  @HiveField(0)
  String name;

  @HiveField(1)
  HiveList friends;

  Person(this.name);

  String toString() => name; // 用于打印
}

class PersonAdapter extends TypeAdapter<Person> {
  @override
  final typeId = 0;

  @override
  Person read(BinaryReader reader) {
    return Person(reader.read())..friends = reader.read();
  }

  @override
  void write(BinaryWriter writer, Person obj) {
    writer.write(obj.name);
    writer.write(obj.friends);
  }
}

首先,我们在 persons box 中存储了三个人,Mario 、 Luna 和 Alex。

接着,我们创建了一个 HiveList ,它包含 Mario 的朋友。HiveList 构造方法需要包含朋友列表的 HiveObject 。 该列表必须不能移动到其它 HiveObject 。第二个参数是 box,包含列表的项目。

当从 box 中删除一个对象时,也会从所有的 HiveLists 中删除。如果从 HiveList 删除一个对象,它仍然会留在 box 中。


手动创建适配器

有时候,创建自定义的 TypeAdapter 很有必要。可以通过继承 TypeAdapter 类实现。要确保指定了通用的参数。

要彻底地测试自定义的 TypeAdapter 。如果它没有正确工作,可能会破坏 box 。

实现一个 TypeAdapter 非常简单。记住 TypeAdapter 必须是不可变的!这里有一个在 Hive 内部使用的 DateTimeAdapter :

class DateTimeAdapter extends TypeAdapter<DateTime> {
  @override
  final typeId = 16;

  @override
  DateTime read(BinaryReader reader) {
    final micros = reader.readInt();
    return DateTime.fromMicrosecondsSinceEpoch(micros);
  }

  @override
  void write(BinaryWriter writer, DateTime obj) {
    writer.writeInt(obj.microsecondsSinceEpoch);
  }
}Copy to clipboardErrorCopied

到 Hive 1.3.0 为止,所有的适配器需要 typeId 的实例变量!

typeId 实例变量分配了用于注册适配器的编号。它在所有的适配器中必须是唯一的。当从磁盘读取对象时 read() 方法会被调用。使用 BinaryReader 读取对象的所有属性。在上面的示例中,只是一个包含 microsecondsSinceEpoch 的 int (整数)。

write() 方法也是一样的,只是它用于向磁盘写入对象。

确保按照之前写入属性的相同顺序读取属性。