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

reindeer-macros

一个基于纯Rust编写的小型基于实体的嵌入式数据库,具有最小的无SQL关系模型

1 个不稳定版本

0.3.0 2023年3月10日

#23#sled-database


用于 reindeer

MIT 许可证

33KB
299

Reindeer 🦌

Reindeer 🦌 提升您的 sled

sledserdebincode 上面的一个小型结构层

Reindeer 是一个小型嵌入式实体存储,基于 sled 构建,使用 serdebincode 进行序列化,完全使用 Rust 编写。

它提供了一个方便的中间层,用于在具有最小关系模型的嵌入式数据库中存储、检索和更新结构体。

入门指南

创建一个 sled 数据库

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

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

从那里,你有两种选择

  • 推导 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>,以及任何由这些类型组成的2元素元组,它都已经实现了,所以您不需要自己实现它。

  • 键代表用于在数据库中标识每个struct实例的唯一键,以检索和更新它们,其类型为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能够处理实体条目的安全删除。如果没有注册,尝试删除未注册的实体条目将导致错误。

将实例保存到数据库

现在您可以将struct实例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 特性必须处于作用域内。

依赖项

~1.5–2MB
~49K SLoC