作者:京东科技 贾世闻
最近在折腾rust与数据库集成,为了偷懒,选了Tidb Cloud Serverless Tier 作为数据源。Tidb 无疑是近五年来最优秀的国产开源分布式数据库,Tidb Cloud Serverless Tier作为pingcap旗下的云产品方便又经济,这次使用还有一些小惊喜,这个后文再说。
Tidb Cloud Serverless Tier 的使用文档还是很全面的,详细情况请参考使用 TiDB Cloud (Serverless Tier) 构建 TiDB 集群.
集群建立完成后,Tidb Cloud Serverless Tier 有个小功能是可以显示主流客户端以及流行编程语言的连接代码。包括: MysqlCli、MyCli、JDBC、Python、golang以及Nodejs。
嗯?rust 的代码在哪儿?很遗憾没有rust的代码。而且为了安全起见,Tidb Cloud Serverless Tier 貌似只支持安全连接。在查找文档过程中rust 的 数据库驱动和很多orm文档中也没有关于安全详细的描述,不少思路是在issues里面给出的。索性把rust 连接 mysql 主流方式的安全连接代码都记录下来,一来给自己留个备忘,二来给需要的同学做个提示。
以下实例所使用的的标的建表语句如下
1CREATE TABLE IF NOT EXISTS sample ( 2 id BIGINT NOT NULL , 3 name VARCHAR(128) NOT NULL, 4 gender TINYINT NOT NULL, 5 mobile VARCHAR(11) NOT NULL, 6 create_time DATETIME NOT NULL, 7 update_time DATETIME NOT NULL, 8 PRIMARY KEY(id) 9 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
mysql rust driver
rust-mysql-simple,纯 rust 实现的 mysql 驱动。
-
依赖
1[dependencies] 2# mysql origin 3mysql = "*" -
代码
1use chrono::Local; 2use mysql::prelude::*; 3use mysql::*; 4use rbatis::snowflake::new_snowflake_id; 5use serde::Deserialize; 6use serde::Serialize; 7 8pub const TABLE_NAME: &str = "sample"; 9 10#[derive(Clone, Debug, Serialize, Deserialize)] 11pub struct BizOrigin { 12 pub id: i64, 13 pub name: String, 14 pub gender: u8, 15 pub mobile: String, 16 pub create_time: Option<String>, 17 pub update_time: Option<String>, 18} 19 20fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { 21 let fmt = "%Y-%m-%d %H:%M:%S"; 22 // 原生方式连接 23 let cert_path = std::path::Path::new("/etc/ssl/cert.pem"); 24 let ssl_opts = SslOpts::default().with_root_cert_path(Some(cert_path)); 25 let opts = OptsBuilder::new() 26 .ip_or_hostname(Some("gateway01.us-east-19.prod.aws.tidbcloud.com")) 27 .tcp_port(4000) 28 .user(Some("tidbcloudtier.root")) 29 .pass(Some("xxxxxxxxxxxx")) 30 .ssl_opts(ssl_opts) 31 .db_name(Some("test")); 32 33 let mut conn_origin = Conn::new(opts)?; 34 let (_, cipher_origin): (Value, String) = "SHOW STATUS LIKE 'Ssl_cipher'" 35 .first(&mut conn_origin)? 36 .unwrap(); 37 println!(">>>>> Cipher in use from origin: {}", cipher_origin); 38 39 let create_statment = format!( 40 " 41 CREATE TABLE IF NOT EXISTS {} ( 42 id BIGINT NOT NULL , 43 name VARCHAR(128) NOT NULL, 44 gender TINYINT NOT NULL, 45 mobile VARCHAR(11) NOT NULL, 46 create_time DATETIME NOT NULL, 47 update_time DATETIME NOT NULL, 48 PRIMARY KEY(id) 49 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;", 50 TABLE_NAME 51 ); 52 conn_origin.query_drop(create_statment)?; 53 54 let bizes = vec![ 55 BizOrigin { 56 id: new_snowflake_id(), 57 name: "Bob".to_string(), 58 gender: 1, 59 mobile: "13037777876".to_string(), 60 create_time: Some(Local::now().format(fmt).to_string()), 61 update_time: Some(Local::now().format(fmt).to_string()), 62 }, 63 BizOrigin { 64 id: new_snowflake_id(), 65 name: "Jecika".to_string(), 66 gender: 0, 67 mobile: "13033457876".to_string(), 68 create_time: Some(Local::now().format(fmt).to_string()), 69 update_time: Some(Local::now().format(fmt).to_string()), 70 }, 71 ]; 72 73 conn_origin.exec_batch( 74 r"insert into sample (id,name,gender,mobile,create_time,update_time) 75 values (:id,:name,:gender,:mobile,:create,:update)", 76 bizes.iter().map(|p| -> Params { 77 params! { 78 "id"=>p.id, 79 "name"=>p.name.to_owned(), 80 "gender"=>p.gender.to_owned(), 81 "mobile"=>p.mobile.to_owned(), 82 "create"=>p.create_time.as_ref(), 83 "update"=>p.update_time.as_ref() 84 } 85 }), 86 )?; 87 88 // Let's select payments from database. Type inference should do the trick here. 89 let selected_bizs = conn_origin.query_map( 90 "SELECT id,name,gender,mobile,create_time,update_time from sample", 91 |(id, name, gender, mobile, create_time, update_time)| BizOrigin { 92 id, 93 name, 94 gender, 95 mobile, 96 create_time, 97 update_time, 98 }, 99 )?; 100 println!("selected result {:?}", selected_bizs); 101 102 Ok(()) 103}
代码并不复杂,首先创建SslOpts,指定CA文件的位置;然后使用OptsBuilder 生成链接配置信息;最后创建Connection。后面是执行表创建以及验证链接,最后是对标的 insert 和 select 操作。
sqlx
sqlx是纯 Rust 编写的异步 SQL Crate。
-
依赖
1[dependencies] 2# sqlx 3sqlx = "0.6.2" -
代码
1use futures::TryStreamExt; 2use sqlx::mysql::MySqlPoolOptions; 3 4#[tokio::main] 5async fn main() { 6 let sqlx_opts = sqlx::mysql::MySqlConnectOptions::new() 7 .host("gateway01.us-east-19.prod.aws.tidbcloud.com") 8 .port(4000) 9 .database("test") 10 .username("tidbcloudtier.root") 11 .password("xxxxxxxxxxxx") 12 .ssl_ca("/etc/ssl/cert.pem"); 13 14 let pool = MySqlPoolOptions::new() 15 .connect_with(sqlx_opts) 16 .await 17 .unwrap(); 18 19 let mut rows = sqlx::query("select * from sample").fetch(&pool); 20 while let Some(row) = rows.try_next().await.unwrap() { 21 println!("row is {:?}", row); 22 } 23}
SeaORM
SeaORM是在 sqlx 之上构建的 orm 框架。
-
依赖
1[dependencies] 2# SeaORM 3sqlx = "0.6.2" 4sea-orm = { version = "0.10.6", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "macros" ] } -
代码
1use sea_orm::ConnectionTrait; 2use sea_orm::DbBackend; 3use sea_orm::SqlxMySqlConnector; 4use sea_orm::{FromQueryResult, Statement as sea_statment}; 5use sqlx::MySqlPool; 6 7#[derive(Debug, FromQueryResult)] 8pub struct SeaOrmBiz { 9 pub id: i64, 10 pub name: String, 11 pub gender: Option<i8>, 12 pub mobile: String, 13 pub create_time: chrono::NaiveDateTime, 14 pub update_time: chrono::NaiveDateTime, 15} 16 17#[tokio::main] 18async fn main() { 19 let sqlx_opts = sqlx::mysql::MySqlConnectOptions::new() 20 .host("gateway01.us-east-19.prod.aws.tidbcloud.com") 21 .port(4000) 22 .database("test") 23 .username("tidbcloudtier.root") 24 .password("xxxxxxxxx") 25 .ssl_ca("/etc/ssl/cert.pem"); 26 27 let pool = MySqlPool::connect_with(sqlx_opts).await.unwrap(); 28 let db = SqlxMySqlConnector::from_sqlx_mysql_pool(pool); 29 30 let rs = db 31 .execute(sea_statment::from_string( 32 db.get_database_backend(), 33 "select 1 from dual;".to_string(), 34 )) 35 .await; 36 println!(">>>>> Cipher in use from sea_orm:{:?}", rs); 37 38 let biz: Vec<SeaOrmBiz> = SeaOrmBiz::find_by_statement(sea_statment::from_sql_and_values( 39 DbBackend::MySql, 40 r#"SELECT * FROM sample;"#, 41 vec![], 42 )) 43 .all(&db) 44 .await 45 .unwrap(); 46 println!(">>>>> selet rs is {:?}", biz); 47}
SeaOrm 依赖 sqlx。首先构建 sqlx::MySqlConnectOptions 然后根据 MySqlConnectOptions 构建 sqlx::MySqlPool 最后构建 sea_orm::SqlxMySqlConnector 用于与 mysql 通信。
Rbatis
-
依赖
1[dependencies] 2# rbatis integration 3rbs = "0.1.13" 4rbatis = "4.0.44" 5rbdc-mysql = "0.1.18" -
代码
1use rbatis::rbdc::datetime::FastDateTime; 2use rbatis::Rbatis; 3use rbdc_mysql::options::MySqlConnectOptions; 4use rbdc_mysql::{driver::MysqlDriver, options::MySqlSslMode as rbdc_MysqlSslMode}; 5use rbs::to_value; 6use serde::{Deserialize, Serialize}; 7use std::collections::HashMap; 8 9pub const TABLE_NAME: &str = "sample"; 10 11#[derive(Clone, Debug, Serialize, Deserialize)] 12pub struct BizRbatis { 13 pub id: Option<i64>, 14 pub name: Option<String>, 15 pub gender: Option<u8>, 16 pub mobile: Option<String>, 17 pub create_time: Option<FastDateTime>, 18 pub update_time: Option<FastDateTime>, 19} 20rbatis::crud!(BizRbatis {}, TABLE_NAME); 21 22#[tokio::main] 23async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { 24 // rbatis 连接 25 let rb = Rbatis::new(); 26 let opt = MySqlConnectOptions::new() 27 .host("gateway01.us-east-19.prod.aws.tidbcloud.com") 28 .port(4000) 29 .database("test") 30 .username("tidbcloudtier.root") 31 .password("xxxxxxxxxx") 32 .ssl_mode(rbdc_MysqlSslMode::VerifyIdentity) 33 .ssl_ca("/etc/ssl/cert.pem"); 34 rb.init_opt(MysqlDriver {}, opt).unwrap(); 35 rb.get_pool().unwrap().resize(3); 36 37 let sql_show_ssl_cipher = "SHOW STATUS LIKE 'Ssl_cipher'"; 38 39 let cipher_rbatis = rb 40 .fetch_decode::<Vec<HashMap<String, String>>>(sql_show_ssl_cipher, vec![]) 41 .await; 42 43 println!(">>>>> Cipher in use from rbatis: {:?}", cipher_rbatis); 44 45 let sql_select_one = format!("select * from {} limit ?;", TABLE_NAME); 46 let row = rb 47 .fetch_decode::<BizRbatis>(&sql_select_one, vec![to_value!(1)]) 48 .await; 49 println!(">>>>> rbatsis select result={:?}", row); 50 51 Ok(()) 52}
首先,新建一个Rbatis struct;构建 rbdc_mysql::options::MySqlConnectOptions (rbdc 相当于java体系里的jdbc,是rbatis的衍生项目);最后通过配置好的 rbdc_mysql::options::MySqlConnectOptions 初始化 Rbatis。
后记
在这次实验中笔者也试图使用Diesel建立 mysql 安全连接,不过在编译的时候失败,未入门先放弃。Diesel 由于开发时间久远,彼时各个数据库的 rust 原生驱动缺失,所以大量才用 c/c++ driver进行构建,这次编译失败也是因为在macos上找不到 mysqlclient 导致。有对 Diesel 强依赖的同学可以继续探索。 再来说说对 SeaOrm 和 Rbatis 的直观感受。SeaOrm 构建实体比较麻烦,如果不是通过工具手工构建实体比较烧脑;实体中包含各种与其他实体的关系;动态sql 可以通过 sea_query 工具包来构建。Rbatis 构建实体心智负担就小很多,一张表一个实体;动态 sql 可以通过 HtmlSql 和 PySql 实现,sql 与代码充分解耦。rbdc 作为 Rbatis 的衍生项目,显然是要做 rust 生态的JDBC。从感觉上来讲 SeaOrm 更像 hibernate;而 Rbatis 是复刻 Mybatis。 数据库是应用程序打交道最多的外部资源,相关话题也很多,有机会再和大家聊聊 rust 与 数据库打交道的更多细节。
咱们下期见。
