作者: 李大狗


本项目代码见:

https://github.com/leeduckgo/weid-rust-sample

本系列代码见:

https://github.com/leeduckgo/Rust-Study

在上一篇文章中,我们使用 Rust 和 FISCO BCOS 开源框架中的 WeBase 进行了交互,使用了 reqwest 这个 Rust 中的 http 库,同时介绍了 Rust 项目中模块分离的设计。

今天,我们将结合 FISCO BCOS 生态中的数字身份组件 WeIdentity,来讲 Rust 中的数据库操作。

1 什么是 数字身份标识与数字身份体系?

首先,让我们来看下什么是分布式数字身份标识(DID):

分布式数字标识符(DID)是一种新型标识符,用以标识可验证的分布式的数字身份。 DID的控制者决定标识的主体(例如,人,组织,事物,数据模型,抽象实体等)。

— W3C DID规范

围绕分布式数字身份标识,我们可以构建如下组件:

  • DID 文档——用以对该 DID 相关的地址、服务以及其它特性进行进一步的阐述。
  • 可验证声明——通过可验证声明,DID 控制者可以发放、持有、验证电子证书与电子凭证。
  • 选择型披露——结合隐私保护技术,DID 控制者可以在保障自己隐私的情况下,向需要的第三方选择性披露自己的数据,如证明自己的年龄大于 18 岁。
  • 数据存证——将可信数据和 DID 进行挂钩,便完整的形成了「数字身份—数字凭证—数据存证」的体系,如「学生身份—毕业证书—课堂表现」。

这便是数字身份体系

2 什么是 WeIdentity?

WeIdentity是一套分布式多中心的技术解决方案,可承载实体对象(人或者物)的现实身份与链上身份的可信映射、以及实现实体对象之间安全的访问授权与数据交换。WeIdentity由微众银行自主研发并完全开源,秉承公众联盟链整合资源、交换价值、服务公众的理念,致力于成为链接多个垂直行业领域的分布式商业基础设施,促进泛行业、跨机构、跨地域间的身份认证和数据合作。

WeIdentity 目前主要包含两大模块:WeIdentity DID 以及 WeIdentity Credential。

WeIdentity 参考场景:

Rust 学习笔记系列| Part 5 - 图1

3 WeIdentity DID规范

WeIdentity 对基本的 DID 规范进行了扩展处理。

基本的 DID 规范:

DID是一个简单的文本字符串,由三部分组成:
1)DID过的URI方案标识符(Scheme,固定就是 did)
2)DID方法的标识符(DID Method)
3)DID方法生成的标识符(DID Method-Specific Identifier)
Rust 学习笔记系列| Part 5 - 图2

扩展后的 WeIdentity:

Rust 学习笔记系列| Part 5 - 图3

字段 说明
did 遵循DID规范,使用固定字符“did”
weid WeIdentity DID规范的method name字段,固定为“weid”
chain-id 链 ID,用于路由到不同的链网络(如果需要跟其他链打通,需要找 WeIdentity 开源项目的 owner 微众银行注册路由信息),例如同时使用 WeIdentity 的可能有多条区块链,可以使用这个字段作为标识信息,路由到特定区块链
bs-specific-string 基于底层区块链平台生成,代表Entity在链上的地址,保证全网唯一

备注:bsSpecificString根据区块链底层平台和业务具体情况来确定生成规则,例如可以是随机字符串,或者区块链上的地址。

示例(这个例子中,chain-id是101: "did:weid:101:0x0086eb1f712ebc6f1c276e12ec21"

4 数据结构设计

在今天的实践中,我们希望可以把weid保存到本地数据库中。

我们这次选择的是 Sqlite 数据库,在Rust - ORM 选择上,我们选择的是 Diesel,这个项目有 6.8k Stars。

https://github.com/diesel-rs/diesel

Tips: 接触新库时,我们可以通过学习 Repo 中的 Examples,来掌握 Repo 的用法。

因为weid中,前半部分did:weid是不变的,所以我们只需保存chain_id`bs-specific-string即可。

在Rust中数据结构如下:

  1. pub struct Weid {
  2. id: i32,
  3. chain_id: i32, //当然也可以同样设置为 String
  4. addr: String,
  5. created_at: NaiveDateTime, // 创建时间
  6. updated_at: NaiveDateTime, // 更新时间
  7. }

数据库的创建语句如下:

  1. CREATE TABLE weids (
  2. id INTEGER PRIMARY KEY AUTOINCREMENT,
  3. addr TEXT NOT NULL,
  4. chain_id INTEGER,
  5. created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  6. updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
  7. );

我们用标准规范创建数据库迁移文件夹migration

  1. migrations
  2. └── 2020-05-13-105400_create_weids
  3. ├── down.sql
  4. └── up.sql

其中,up.sql的内容即是上面的内容:

  1. CREATE TABLE weids (
  2. id INTEGER PRIMARY KEY AUTOINCREMENT,
  3. addr TEXT NOT NULL,
  4. chain_id INTEGER,
  5. created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  6. updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
  7. );

down.sql的内容就是移除weids表:

  1. DROP TABLE weids;

5 数据库的创建与建表(Create&Migrate)

此处我们使用diesel使用的命令行工具。

(1)复制必要文件到项目目录下

从 Diesel 的代码仓库中 Clone 代码到本地。

https://github.com/diesel-rs/diesel

将 Repo 中的 dieseldiesel_clidiesel_derivesdiesel_migrations复制到项目根目录下。

在项目根目录下新建bin文件夹。

编译 Diesel Repo,将target/debug/diesel文件复制到bin目录下。

(2)配置环境变量

我们需要设置两个环境变量:

  • DATABASE_URL——数据库路径
  • BACKEND———数据库类型

直接执行如下命令即可:

  1. export DATABASE_URL="examples.db"
  2. export BACKEND="sqlite"

(3)创建数据库与建表

执行如下命令:

  1. ./bin/diesel database reset

顺利的话,会出现如下返回:

Rust 学习笔记系列| Part 5 - 图4

同时根目录下出现examples.db文件。

6 models.rs

我们在src目录下创建models.rs文件,在其中定义结构体WeidNewWeid,定义 schema(模式)weids

Scheme,可以简单的理解为我们告诉程序数据库中有哪些字段,这样程序才能顺利对接数据库。

models.rs

  1. use chrono::NaiveDateTime;
  2. #[cfg(test)]
  3. use diesel::debug_query;
  4. use diesel::insert_into;
  5. use diesel::prelude::*;
  6. #[cfg(test)]
  7. use diesel::sqlite::Sqlite;
  8. use serde_derive::Deserialize;
  9. use std::error::Error;
  10. pub mod schema {
  11. diesel::table! {
  12. weids {
  13. id -> Integer,
  14. chain_id -> Integer,
  15. addr -> Text,
  16. created_at -> Timestamp,
  17. updated_at -> Timestamp,
  18. }
  19. }
  20. }
  21. use schema::weids;
  22. #[derive(Insertable)]
  23. #[table_name = "weids"]
  24. pub struct NewWeid {
  25. pub chain_id: i32,
  26. pub addr: String,
  27. }
  28. #[derive(Queryable, PartialEq, Debug)]
  29. pub struct Weid {
  30. pub id: i32,
  31. pub chain_id: i32,
  32. pub addr: String,
  33. pub created_at: NaiveDateTime,
  34. pub updated_at: NaiveDateTime,
  35. }
  36. pub fn insert_default_values(conn: &SqliteConnection) -> QueryResult<usize> {
  37. use schema::weids::dsl::*;
  38. insert_into(weids).default_values().execute(conn)
  39. }

代码解析:

注: 以下内容对《Rust 程序设计语言(第一版)》有所参考。

https://kaisery.gitbooks.io/rust-book-chinese/content/content/Traits.html

trait 是一个告诉 Rust 编译器一个类型必须提供哪些功能语言特性。

例如,我们可以为结构体Circle实现HasArea这个trait

  1. struct Circle {
  2. x: f64,
  3. y: f64,
  4. radius: f64,
  5. }
  6. trait HasArea {
  7. fn area(&self) -> f64;
  8. }
  9. impl HasArea for Circle {
  10. fn area(&self) -> f64 {
  11. std::f64::consts::PI * (self.radius * self.radius)
  12. }
  13. }

如你所见,trait块与impl看起来很像,不过我们没有定义一个函数体,只是函数标记。当我们impl一个trait时,我们使用impl Trait for Item,而不是仅仅impl Item

重复的实现像DebugDefault这样的 trait 会变得很无趣。为此,Rust 提供了一个属性 属性.md)来允许我们让 Rust 为我们自动实现 trait:

  1. #[derive(Debug)]
  2. struct Foo;
  3. fn main() {
  4. println!("{:?}", Foo);
  5. }

Rust 1.15中引入了自定义derive特性,从而让derive有了更多的想象空间。

我们通过#[derive(Insertable)]#[derive(Queryable, PartialEq, Debug)],让该结构体具备可插入数据库,或从数据库查询的特性。

7 main.rs

main.rs 的内容如下所示:

  1. extern crate pretty_env_logger;
  2. pub mod models;
  3. use diesel::prelude::*;
  4. use std::env;
  5. use dotenv::dotenv;
  6. use weid_light_client::WeIdRestService;
  7. use models::*;
  8. use models::schema::weids;
  9. use models::schema::weids::dsl::*;
  10. #[macro_use] extern crate log;
  11. fn main(){
  12. pretty_env_logger::init();
  13. // create data
  14. let sqlite_conn = establish_connection();
  15. create_weid(&sqlite_conn, 1, "34be11396f3a91c5Ab5A1220e756C6300FB2b20a");
  16. // query data
  17. let results = weids.load::<Weid>(&sqlite_conn)
  18. .expect("Error loading weids");
  19. // log weids
  20. info!("Displaying {} weids", results.len());
  21. for weid in results{
  22. info!("did:weid:{}:{}", weid.chain_id, weid.addr);
  23. }
  24. }
  25. pub fn establish_connection() -> SqliteConnection {
  26. dotenv().ok();
  27. let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
  28. SqliteConnection::establish(&database_url)
  29. .unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
  30. }
  31. pub fn create_weid(conn: &SqliteConnection, c_id: i32, address: &str) -> usize {
  32. let new_weid = NewWeid {chain_id: c_id, addr: address.to_string()};
  33. diesel::insert_into(weids::table)
  34. .values(&new_weid)
  35. .execute(conn)
  36. .expect("Error saving new weid")
  37. }

代码解析:

establish_connection直接拷贝自 Diesel 的Examples,作用是根据环境变量中的DATABASE_URL连接sqlite数据库。

create_weid函数中,我们先创建一个 NewWeid 结构体对象,然后通过diesel::insert_into函数将新建的结构体对象插入数据库。

通过loads函数,我们从数据库中加载Weid结构体。

8 运行

执行RUST_LOG=trace cargo run

我们成功向数据库插入一条 weid 数据,并读取 出weid 数据。

Rust 学习笔记系列| Part 5 - 图5

关于diesel的更多更详细的用法,见:

https://diesel.rs/guides/getting-started