#entity #sled-database #sled #bincode #embedded-database #rocksdb

驯鹿

一个基于纯Rust编写的最小化NoSQL关系模型的小型基于实体的嵌入式数据库

19个版本

0.3.0 2023年3月10日
0.2.7 2022年9月8日
0.2.3 2022年8月16日
0.1.11 2022年8月6日
0.1.4 2022年6月21日

#732数据库接口

每月27次下载

MIT 许可证

105KB
2K SLoC

驯鹿 🦌

驯鹿 🦌 提升你的 sled

sledserdebincode 之上的一个小型结构层

驯鹿是一个小型嵌入式实体存储库,建立在 sled 之上,使用 serdebincode 进行序列化,完全使用 Rust 编写。

它作为存储、检索和更新嵌入式数据库中结构体的便捷中间层。

入门

创建一个 sled 数据库

use reindeer::Db;
let db = reindeer::open("./my-db")?;

💡 由于这是一个 sled 数据库,这个对象可以安全地在线程之间复制和发送。

从这里,你有两种选择

  • 派生 Entity 特性
  • 手动实现特性。

手动实现 Entity 特性

使用 derive

实体需要实现来自 serdeSerializeDeserialize 特性,这些特性方便地从 reindeer 中重新导出

use reindeer::{Serialize,Deserialize,Entity}

#[derive(Serialize,Deserialize,Entity)]
pub struct MyStruct {
    pub id : u32,
    pub prop1 : String,
    pub prop2 : u64,
}

如果你的结构体已经有了 id 字段,则它将用作存储的键。其类型必须是整数类型、StringVec<u8>,或这些类型的元组。

数据库中存储的名称将是实体的名称,保留其原始大小写。在这种情况下,请确保它是唯一具有此名称的实体。

否则,您可以使用 entity 辅助属性来指定不同的键字段和名称

#[derive(Serialize,Deserialize,Entity)]
#[entity(name = "user", id = "email")]
pub struct User {
    pub email : String,
    pub prop1 : String,
    pub prop2 : u64,
}

通过手动实现 Entity 特性

实体需要实现来自 serdeSerializeDeserialize 特性,这些特性方便地从 reindeer 中重新导出

use reindeer::{Serialize,Deserialize,Entity}

#[derive(Serialize,Deserialize)]
pub struct MyStruct {
    pub id : u32,
    pub prop1 : String,
    pub prop2 : u64,
}

然后您需要实现Entity特质,并实现三个方法:get_keyset_keystore_name,以及定义一个关联类型Key

  • Key是您实体每个实例的标识符的类型("主键")。它必须实现AsBytes特质。😌☝对于Stringu32i32u64i64以及Vec<u8>以及这些类型的任意两个元素的元组,它已经实现,因此您不需要自行实现。

  • 键代表在数据库中标识每个结构实例的唯一键,用于检索和更新它们,它的类型是Key

  • store_name是实体存储的名称。它应该对每种实体类型唯一(可以将其视为表名)。

use reindeer::{Entity, Serialize,Deserialize};

#[derive(Serialize,Deserialize)]
struct MyStruct  { key : u32, prop1 : String }

impl Entity for MyStruct{
    type Key = u32;
    fn store_name() -> &'static str {
        "my-struct"
    }
    fn get_key(&self) -> &Self::Key {
        &self.key
    }
    fn set_key(&mut self, key : &Self::Key) {
        self.key = key.clone();
    }
 }

在系统中注册您的实体

在您启动应用程序时注册实体一次。

let db = reindeer::open("./my-db")?;
MyStruct::register(db)?;

💡 注册实体将使Reindeer能够处理实体条目的安全删除。如果没有注册,尝试删除未注册的实体条目将导致错误。

将实例保存到数据库

现在您可以将您的结构实例MyStruct保存到数据库中

let db = reindeer::open("./")?;
let instance = MyStruct {
    id : 0,
    prop1 : String::from("Hello"),
    prop2 : 2335,
}
instance.save(&db)?;

💡 如果数据库中已存在ID为0的记录,它将被覆盖!

从数据库检索实例

let instance = MyStruct::get(0,&db)?;

检索所有实例

let instances = MyStruct::get_all(&db)?;

根据条件获取所有实体

let instances = MyStruct::get_with_filter(|m_struct| {mstruct.prop1.len > 20},&db)?;

从数据库中删除实例

MyStruct::remove(0,&db)?;

定义关系

reindeer有三种关系类型

  • sibling:具有另一个存储中相同键的实体(一对一关系)
  • 父子关系:键由其父键和一个u32(作为两个元素的元组)组成,用于高效的父子关系
  • 自由关系您可以将两个不同实体的两个实例自由地连接起来。这可以用来实现多对多关系,但在查询数据库方面不如兄弟和父子关系高效。**当兄弟和父子关系不可用时使用**。

兄弟关系

要创建兄弟实体,您需要通过覆盖指定兄弟存储以及删除其中一个时的行为来将实体结构相互关联。

兄弟存储必须共享相同的键类型(因此匹配的实体将具有相同的ID)。

💡 删除行为决定了当当前实体被删除时兄弟将发生什么

  • DeletionBehaviour::Cascade会删除兄弟实体
  • DeletionBehaviour::Error如果兄弟仍然存在,则引发错误且不会删除源元素
  • DeletionBehaviour::BreakLink只是删除实体而不删除其兄弟

使用derive

您可以使用siblings辅助属性指定兄弟

#[derive(Serialize,Deserialize,Entity)]
#[entity(name = "user", id = "email")]
#[siblings(("user_data",Cascade),("user_data2",Cascade))]
pub struct User {
    pub email : String,
    pub prop1 : String,
    pub prop2 : u64,
}

#[derive(Serialize,Deserialize,Entity)]
#[entity(name = "user_data", id = "email")]
#[siblings(("user",Error),("user_data2",Cascade))]
pub struct UserData {
    pub email : String,
    pub prop3 : String,
    pub prop4 : String,
    pub prop5 : i64
}

在上面的例子中,删除一个User实例也会删除其兄弟UserData实例,但删除UserData实例将引发错误且不会删除任何实例。

手动

use reindeer::{Entity,DeletionBehaviour};
impl Entity for MyStruct1{
    /* ... */
    fn store_name() -> &'static str {
        "my_struct_1"
    }
    fn get_sibling_stores() -> Vec<(&'static str,DeletionBehaviour)> {
        return vec![("my_struct_2",DeletionBehaviour::Cascade)]
    }
}

impl Entity for MyStruct2{
    /* ... */
    fn store_name() -> &'static str {
        "my_struct_2"
    }
    fn get_sibling_stores() -> Vec<(&'static str,DeletionBehaviour)> {
        return vec![("my_struct_1",DeletionBehaviour::BreakLink)]
    }
}

💡 如果定义了兄弟树,实体实例可能或可能没有来自另一个兄弟存储的兄弟!默认情况下,兄弟是可选的。

在上面的例子中,删除一个 MyStruct1 实例也会删除其同级 MyStruct2 实例,但删除 MyStruct2 实例则不会影响其同级 MyStruct1 实例。

💡 同级实体必须具有相同的 Key 类型。

创建同级实体

let m_struct_1 = MyStruct1 {
    /* ... */
};
let mut m_struct_2 = MyStruct2 {
    /* ... */
};
m_struct_1.save(&db)?;
m_struct_1.save_sibling(m_struct_2,&db)?;

💡 这将使用 set_key 方法将 m_struct_2 的键更新为 m_struct_1 的键,因此调用 save_child 之前提供哪个键都无关紧要。

⚠️ 注意,如果您在 MyStruct2 的存储中创建一个与 MyStruct1 的存储中的实体具有相同键的实体,而没有使用 save_sibling,则结果相同,并且这两个实体将被视为同级。

检索同级实体

if let Some(sibling) = m_struct_1.get_sibling::<MyStruct2>(&db)? {
    /* ... */
}

💡 注意,同级实体可能存在也可能不存在,因此使用 Option 类型。

父子关系

为了在实体之间建立父子关系,子实体必须有一个 Key 类型,它是一个

  • Key 类型
  • u32

💡 子实体将自动递增,并且可以通过其父键轻松检索。

使用 derive

您可以像定义同级存储一样定义子存储,但使用 children 辅助属性

#[derive(Serialize,Deserialize,Entity)]
#[entity(name = "user", id = "email")]
#[children(("document",Cascade))]
pub struct User {
    pub email : String,
    pub prop1 : String,
    pub prop2 : u64,
}

#[derive(Serialize,Deserialize,Entity)]
#[entity(name = "document")]
pub struct Document {
    pub id : (String,u32),
    pub prop3 : String,
    pub prop4 : String,
    pub prop5 : i64
}

手动实现

impl Entity for Parent{
    type Key = String;
    /* ... */
    fn store_name() -> &'static str {
        "parent"
    }
    fn get_child_stores() -> Vec<(&'static str)> {
        return vec![("child", DeletionBehaviour::Cascade)]
    }
}

impl Entity for Child{
    type Key = (String, u32);
    /* ... */
    fn store_name() -> &'static str {
        "child"
    }
}

在上面的例子中,删除父实体将自动删除所有子实体(归功于 Cascade 删除行为)。

出于数据库完整性的考虑,强烈建议不要在父/子关系上使用 DeletionBehaviour::BreakLink,而是使用 ErrorCascade

添加子实体

let parent = Parent {
    /* ... */
};

let mut child = Child {
    /* ... */
}

parent.save_child(child,&db)?;

💡 这将使用 set_key 方法将 child 的键更新为 parent 的键和一个自动递增的索引,因此调用 save_child 之前提供哪个键都无关紧要。

获取子实体

let children = parent.get_children::<Child>(&db)?;

自由关系

自由关系遵循与其他关系类型相同的模式,但它们可以自由地创建在任何两个实体之间。这可以用来实现多对多关系。

💡 创建一个自由关系将自动创建其相反关系,使其双向。

链接两个实体

let e1 = Entity1 {
    /* ... */
};

let mut e2 = Entity2 {
    /* ... */
}

e1.create_relation(e2,DeletionBehaviour::Cascade, DeletionBehaviour::BreakLink,None,&db)?;

在上面的例子中,提供了两种方式的删除行为:删除 e1 将自动删除 e2,但删除 e2 将不影响 e1 并断开它们之间的链接。

DeletionBehaviour::Error 也是一个选项。

let related_entities = e1.get_related::<Entity2>(db)?;

要仅从其他树中获取第一个相关实体,请使用

let related_entity = e1.get_single_related::<Entity2>(db)?;

在创建关系时必须提供一个名称

e1.create_relation(e2,DeletionBehaviour::Cascade, DeletionBehaviour::BreakLink,Some("main"),&db)?;
let related_entities = e1.get_related_with_name::<Entity2>("secondary",db)?;

要仅从其他树中获取第一个相关实体,请使用

let related_entity = e1.get_single_related_with_name::<Entity2>("main",db)?;

如果需要,您可以移除实体之间的现有链接

e1.remove_relation(other,db)?;

e1.remove_relation_with_key::<OtherEntity>(otherKey,db)?;

死锁 🔒

当为您的关联定义 DeletionBehaviour 时,请小心 不要创建死锁

例如,如果两个同级相互定义了 DeletionBehaviour::Error 链接,那么它们中的任何一个都无法被删除...

此外,请注意在数据库中创建的循环。虽然您可以安全地创建关系循环,但上述相同的死锁规则同样适用,并且库只有在您尝试删除某些内容时才会检测到。

性能

虽然兄弟和父子关系默认性能良好,但自由关系性能较差,并且依赖于隐藏的对象存储来工作,迫使在关系创建和实体删除时对数据库进行读写。请注意这个陷阱。

另外,定义级联关系将在删除实体时递归地遍历关系,这使得操作比无关系的实体更重。

自增实体

如果您的实体Key类型是u32,您可以使用以下方式来自动增加新实体:

use reindeer::AutoIncrementEntity;
let mut new_entity = Entity {
    id : 0 // if you setup id with any key, saving will update it
    /* ... */
};
new_entity.save_next(db)?;
// new_entity's key is now the auto-incremente value

您的实体键将自动通过set_key更新,以匹配找到的最后一条记录的ID,并加1。

💡 注意,AutoIncrementEntity特质需要处于作用域内。

依赖项

~2.9–4MB
~68K SLoC