#yaml #fixture #seed #database #seeding #database-schema #fixtures

cder

基于 serde 兼容的 yaml 文件创建和持久化结构化实例的数据库种子生成器

5 个版本

0.2.2 2024 年 8 月 14 日
0.2.1 2023 年 12 月 4 日
0.2.0 2023 年 4 月 18 日
0.1.1 2023 年 1 月 25 日
0.1.0 2023 年 1 月 24 日

#106开发工具

Download history 8/week @ 2024-05-27 6/week @ 2024-06-03 14/week @ 2024-06-17 23/week @ 2024-06-24 10/week @ 2024-07-01 2/week @ 2024-07-08 14/week @ 2024-07-15 43/week @ 2024-07-22 51/week @ 2024-07-29 31/week @ 2024-08-05 140/week @ 2024-08-12

每月 265 次下载

MIT 许可证

37KB
486 代码行

cder Latest version Documentation licence

cder

Rust 轻量级、简单的数据库种子工具


cder (see-der) 是一个数据库种子工具,帮助您在本地环境中导入 fixture 数据。

以编程方式生成种子是一个简单的任务,但维护它们却不是。每次当您的模式发生变化时,您的种子可能会被破坏。这需要您的团队付出额外的努力来保持它们更新。

使用 cder,您可以

  • 以可读的格式维护数据,与种子程序分离
  • 使用 内嵌标签 在线处理引用完整性
  • 重用现有的结构和插入函数,只需要很少的粘合代码

cder 没有数据库交互机制,因此它可以与任何类型的 ORM 或数据库包装器(例如 sqlx)一起工作,您的应用程序已经拥有。

这种内嵌标签机制是受 Ruby on Rails 为测试数据生成提供的 fixtures 启发的。

安装

# Cargo.toml
[dependencies]
cder = "0.2"

用法

快速入门

假设您有用户表作为种子目标

CREATE TABLE
  users (
    `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `name` VARCHAR(255) NOT NULL,
    `email` VARCHAR(255) NOT NULL,
  )

在您的应用程序中您还有

  • 一个类型为 <T> 的结构(通常是模型,基于底层表构建)
  • 数据库插入方法:返回新记录的 id: Fn(T) -> Result<i64>

首先,在结构体上添加DeserializeOwned特性。(cder引入了serde作为依赖,因此可以使用derive(Deserialize)宏来完成这项工作)

use serde::Deserialize;

#[derive(Deserialize)] // add this derive macro
User {
  name: String,
  email: String,
}

impl User {
  // can be sync or async functions
  async fn insert(&self) -> Result<(i64)> {
    //
    // inserts a corresponding record into table, and returns its id when succeeded
    //
  }
}

您的用户种子由两个独立的文件定义,数据和胶水代码。

现在创建一个种子数据文件'fixtures/users.yml'。

# fixtures/users.yml

User1:
  name: Alice
  email: '[email protected]'
User2:
  name: Bob
  email: '[email protected]'

现在您可以将上述两个用户插入到数据库中。

use cder::DatabaseSeeder;

async fn populate_seeds() -> Result<()> {
    let mut seeder = DatabaseSeeder::new()

    seeder
        .populate_async("fixtures/users.yml", |input| {
            async move { User::insert(&input).await }
        })
        .await?;

    Ok(())
}

瞧!您将在数据库中填充记录AliceBob

与非异步函数一起工作

如果您的函数是非异步(普通)函数,请使用Seeder::populate而不是Seeder::populate_async

use cder::DatabaseSeeder;

fn main() -> Result<()> {
    let mut seeder = DatabaseSeeder::new();

    seeder
        .populate("fixtures/users.yml", |input| {
            // this block can contain any non-async functions
            // but it has to return Result<i64> in the end
            diesel::insert_into(users)
                .values((name.eq(input.name), email.eq(input.email)))
                .returning(id)
                .get_result(conn)
                .map(|value| value.into())
        })

        Ok(())
}

构建实例

如果您想在插入之前对反序列化的结构体有更细粒度的控制,请使用StructLoader。

use cder::{ Dict, StructLoader };

fn construct_users() -> Result<()> {
    // provide your fixture filename followed by its directory
    let mut loader = StructLoader::<User>::new("users.yml", "fixtures");

    // deserializes User struct from the given fixture
    // the argument is related to name resolution (described later)
    loader.load(&Dict::<String>::new())?;

    let customer = loader.get("User1")?;
    assert_eq!(customer.name, "Alice");
    assert_eq!(customer.email, "[email protected]");

    let customer = loader.get("User2")?;
    assert_eq!(customer.name, "Bob");
    assert_eq!(customer.email, "[email protected]");

    ok(())
}

动态定义值

cder根据一些规则替换了某些标签的值。这种'预处理'在反序列化之前运行,因此您可以定义依赖于您本地环境的动态值。

目前涵盖了以下两种情况

1. 定义关系(外键)

假设您有两条记录要插入到companies表中。companies.id是未知的,因为它们在插入时由本地数据库提供。

# fixtures/companies.yml

Company1:
  name: MassiveSoft
Company2:
  name: BuggyTech

现在您有引用这些公司的用户记录

# fixtures/users.yml

User1:
  name: Alice
  company_id: 1 // this might be wrong

您可能会在构建User1时失败,因为Company1不保证id=1(尤其是在您已经操作过companies表的情况下)。为此,请使用${{ REF(label) }}标签替换未定的值。

User1:
  name: Alice
  company_id: ${{ REF(Company1) }}

现在,Seeder是如何知道Company1记录的id的?如前所述,给 Seeder 的块必须返回Result<i64>。Seeder将结果值映射到记录标签,稍后将重新使用它来解决标签引用。

use cder::DatabaseSeeder;

async fn populate_seeds() -> Result<()> {
    let mut seeder = DatabaseSeeder::new();
    // you can specify the base directory, relative to the project root
    seeder.set_dir("fixtures");

    // Seeder stores mapping of companies record label and its id
    seeder
        .populate_async("companies.yml", |input| {
            async move { Company::insert(&input).await }
        })
        .await?;
    // the mapping is used to resolve the reference tags
    seeder
        .populate_async("users.yml", |input| {
            async move { User::insert(&input).await }
        })
        .await?;

    Ok(())
}

注意事项

  1. 先插入包含'引用'记录的文件(如上面的示例中的'companies'),然后再插入'引用'记录(如'users')。
  2. 目前 Seeder 在读取源文件时解析标签。这意味着您不能在同一个文件中有对记录的引用。如果您想从一个文件中引用用户记录,可以通过将yaml文件分成两部分来实现。

2. 环境变量

您还可以使用${{ ENV(var_name) }}语法来引用环境变量。

Dev:
  name: Developer
  email: ${{ ENV(DEVELOPER_EMAIL) }}

如果定义了该环境变量,则电子邮件将被替换为DEVELOPER_EMAIL

如果您更喜欢使用默认值,请使用(类似shell的)语法

Dev:
  name: Developer
  email: ${{ ENV(DEVELOPER_EMAIL:-"[email protected]") }}

如果没有指定默认值,所有指向未定义环境变量的标签都将简单地替换为空字符串""。

数据表示

cder根据serde-yaml反序列化yaml数据,支持强大的serde序列化框架。使用serde,您可以反序列化几乎任何结构体。您可以看到一些示例结构体,它们具有各种属性和yaml文件,可以用作它们的种子。

以下是一些所需的YAML格式的要点。有关更多详细信息,请查看serde-yaml的GitHub页面

基础

Label_1:
  name: Alice
  email: '[email protected]'
Label_2:
  name: Bob
  email: '[email protected]'

请注意,cder要求每个记录都必须有标签(Label_x)。标签可以是任何东西(只要它是有效的yaml键),但您可能希望保持它们唯一,以避免意外误引用。

枚举和复杂类型

可以使用YAML的!标签反序列化枚举。假设您有一个名为CustomerProfile的结构体,其中包含枚举Contact

struct CustomerProfile {
  name: String,
  contact: Option<Contact>,
}

enum Contact {
  Email { email: String }
  Employee(usize),
  Unknown
}

您可以根据以下方式生成具有每种联系类型的客户:

Customer1:
  name: "Jane Doe"
  contact: !Email { email: "[email protected]" }
Customer2:
  name: "Uncle Doe"
  contact: !Employee(10100)
Customer3:
  name: "John Doe"
  contact: !Unknown

不推荐用于生产环境

cder旨在填充开发(或可能测试)环境中的种子。不建议用于生产。

许可

该项目根据MIT许可证作为开源软件提供。

贡献

除非您明确声明,否则您提交的任何有意提交以包含在此crate中的贡献,均应按MIT许可证授权,不附加任何额外条款或条件。

欢迎在GitHub上提交错误报告和pull请求:https://github.com/estie-inc/cder

依赖项

~4–6MB
~114K SLoC