2个不稳定版本
0.2.0 | 2024年5月12日 |
---|---|
0.1.0 | 2024年1月15日 |
#392 in 编码
37 每月下载量
37KB
184 行
魔法迁移
自动加载并将反序列化的结构体迁移到最新版本。
🎵 如果你相信魔法,就跟我来
我们将一直跳舞到早上,直到只剩下你和我 🎵
这些文档旨在在 docs.rs 上阅读。
什么是
假设你创建了一个以某种方式序列化到磁盘的结构体;也许它使用toml。现在,假设你想要向该结构体添加一个新字段,但想要保留旧持久数据。你该怎么做?
你可以使用From
或TryFrom
来定义如何从一个结构体转换到另一个结构体,然后通过Migrate
或TryMigrate
特质告诉Rust如何从一版本迁移到下一版本。现在,当你尝试将数据加载到当前结构体时,它将按反向顺序遍历结构体链,以找到第一个可以成功序列化的结构体。当发生这种情况时,它将自动将该结构体转换为最新版本。这是魔法!(实际上,这主要是通过特质边界进行的巧妙使用,但不管怎样)。
文档
有关更多信息,请参阅
-
特质
Migrate
特质用于不可变迁移TryMigrate
特质用于可能失败的迁移
-
TOML链宏:指定迁移链中结构体的顺序
migrate_toml_chain
宏用于不可变迁移TOML数据try_migrate_toml_chain
宏用于可能失败的迁移TOML数据。需要额外的错误结构体。
-
自定义反序列化宏
migrate_deserializer_chain
宏用于不可变迁移,自定义反序列化器try_migrate_deserializer_chain
宏用于不可靠迁移,自带的反序列化器。需要额外的错误结构体。
带有 TryMigrate
的不可靠迁移示例与 try_migrate_deserializer_chain
一旦定义,通过在您希望反序列化的结构体上调用 try_from_str_migrations
关联函数来执行迁移。
要定义迁移,可以使用如下宏
use magic_migrate::{TryMigrate, try_migrate_deserializer_chain};
use serde::de::Deserializer;
// ...
try_migrate_deserializer_chain!(
deserializer: toml::Deserializer::new,
error: PersonMigrationError,
chain: [PersonV1, PersonV2],
);
deserializer:
参数应该是一个函数,它接受一个&str
并返回impl Deserialize<'de>
。要反序列化 TOML,可以使用toml
包并指定toml::Deserializer::new
函数。error:
在反序列化失败时使用的错误枚举。每个TryFrom
应返回一个定义了此提供错误Into
的错误。chain:
一个有序的从左到右的结构体列表,您希望在这些结构体之间迁移。每对结构体必须单独定义它自己的TryFrom
。在这种情况下,只需要一个TryFrom<PersonV1> for PersonV2
。
完整示例
use magic_migrate::{TryMigrate, try_migrate_deserializer_chain};
use serde::de::Deserializer;
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use std::convert::Infallible;
#[derive(Deserialize, Serialize, Debug)]
#[serde(deny_unknown_fields)]
struct PersonV1 {
name: String
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(deny_unknown_fields)]
struct PersonV2 {
name: String,
updated_at: DateTime<Utc>
}
try_migrate_deserializer_chain!( // <=========== HERE
deserializer: toml::Deserializer::new,
error: PersonMigrationError,
chain: [PersonV1, PersonV2],
);
impl TryFrom<PersonV1> for PersonV2 {
type Error = NotRichard;
fn try_from(value: PersonV1) -> Result<Self, NotRichard> {
if &value.name == "Schneems" {
Ok(PersonV2 {
name: value.name.clone(),
updated_at: Utc::now()
})
} else {
Err(NotRichard { name: value.name.clone() })
}
}
}
#[derive(Debug, Eq, PartialEq)]
struct NotRichard {
name: String
}
impl From<NotRichard> for PersonMigrationError {
fn from(value: NotRichard) -> Self {
PersonMigrationError::NotRichard(value)
}
}
#[derive(Debug, Eq, PartialEq)]
enum PersonMigrationError {
NotRichard(NotRichard),
}
// Create a V2 struct from V1 data
let person: PersonV2 = PersonV2::try_from_str_migrations("name = 'Schneems'").unwrap().unwrap();
assert_eq!(person.name, "Schneems".to_string());
提示:您可以通过创建类型别名来减少代码冗余,例如
pub(crate) type Person = PersonV2;
为什么
这个库是为了处理在 https://github.com/heroku/libcnb.rs 构建包中存储为 toml 的序列化元数据的情况。
在这个用例中,当运行云原生构建包 (CNB) 时,结构体会序列化到磁盘。通常,这些值代表应用程序缓存状态,对于缓存失效很重要。
构建包实现者无法控制构建包运行频率。这意味着无法保证最终用户将以连续的结构体版本运行它。一个用户可能使用最新版本的结构体序列化运行,而另一个可能使用多年前的版本。
这种场景在野外与 https://github.com/heroku/heroku-buildpack-ruby(一个“经典”构建包,即不是 CNB)中发生。
而不是强制程序员始终考虑所有可能的缓存状态,一种“迁移”方法允许程序员一次专注于一个缓存状态变化。这减少了程序员的认知负担,并(希望)减少了错误。
它不能做什么?(ABA 问题)
此库不能保证如果 PersonV1
结构体被序列化,则不能在没有迁移的情况下将其加载到 PersonV2
。即它不保证运行了 From
或 TryFrom
代码。
例如,如果PersonV2
结构体引入了一个Option<String>
字段,而不是DateTime<Utc>
,那么字符串"name = 'Richard'"
可以被反序列化为PersonV1或PersonV2,而无需调用迁移。
在Serde的相关讨论中还有更多链接
您能做些什么来强化代码以抵御这种(ABA)问题?
- 使用serde的deny_unknown_fields。此设置防止静默删除额外的结构体字段。此策略将处理V1有两个字段而V2只有一个字段的情况游乐场示例。但是,它将不会保护我们添加了可选字段的情况游乐场示例。
- 添加测试以确保一个结构体不能反序列化为链中的后续结构体。如果您的结构体具有许多可选字段,并且您想生成所有可能的排列,编写测试可能很困难。
- 添加一个版本标记字段。此策略有效,但您必须在创建新结构体时注意并更新字段名称(可能存在程序员错误)。此外,它将实现细节泄露给可能查看您的序列化数据的人(这可能或可能不重要)。
- 阅读这些文档并了解这种发生的原因。
- 如果您有其他强化代码库的建议,请提出问题。
其他可能的“迁移”解决方案及其差异
- 使用Serde的container attributes from and try_from。此功能仅在您永远不想存储和反序列化链中的最新版本时才有效。游乐场示例显示何时失败。
与使用Serde的from
和try_from
容器属性功能相比,magic migrate始终首先尝试转换为目标结构体,然后使用链中的最新可能的结构体进行迁移,允许结构体在整个链中迁移或存储和使用最新值。
- 似乎Serde版本crate有重叠的目标。差异尚不清楚。如果您已尝试它,请更新这些文档。
依赖关系
~110–345KB