文盤Rust -- 安全連線 TiDB/Mysql
作者:京東科技 賈世聞
最近在折騰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 主流方式的安全連線程式碼都記錄下來,一來給自己留個備忘,二來給需要的同學做個提示。
以下例項所使用的的標的建表語句如下
CREATE TABLE IF NOT EXISTS sample (
id BIGINT NOT NULL ,
name VARCHAR(128) NOT NULL,
gender TINYINT NOT NULL,
mobile VARCHAR(11) NOT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
mysql rust driver
rust-mysql-simple,純 rust 實現的 mysql 驅動。
-
依賴
[dependencies] # mysql origin mysql = "*"
-
程式碼
use chrono::Local; use mysql::prelude::*; use mysql::*; use rbatis::snowflake::new_snowflake_id; use serde::Deserialize; use serde::Serialize; pub const TABLE_NAME: &str = "sample"; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct BizOrigin { pub id: i64, pub name: String, pub gender: u8, pub mobile: String, pub create_time: Option<String>, pub update_time: Option<String>, } fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { let fmt = "%Y-%m-%d %H:%M:%S"; // 原生方式連線 let cert_path = std::path::Path::new("/etc/ssl/cert.pem"); let ssl_opts = SslOpts::default().with_root_cert_path(Some(cert_path)); let opts = OptsBuilder::new() .ip_or_hostname(Some("gateway01.us-east-19.prod.aws.tidbcloud.com")) .tcp_port(4000) .user(Some("tidbcloudtier.root")) .pass(Some("xxxxxxxxxxxx")) .ssl_opts(ssl_opts) .db_name(Some("test")); let mut conn_origin = Conn::new(opts)?; let (_, cipher_origin): (Value, String) = "SHOW STATUS LIKE 'Ssl_cipher'" .first(&mut conn_origin)? .unwrap(); println!(">>>>> Cipher in use from origin: {}", cipher_origin); let create_statment = format!( " CREATE TABLE IF NOT EXISTS {} ( id BIGINT NOT NULL , name VARCHAR(128) NOT NULL, gender TINYINT NOT NULL, mobile VARCHAR(11) NOT NULL, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, PRIMARY KEY(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;", TABLE_NAME ); conn_origin.query_drop(create_statment)?; let bizes = vec![ BizOrigin { id: new_snowflake_id(), name: "Bob".to_string(), gender: 1, mobile: "13037777876".to_string(), create_time: Some(Local::now().format(fmt).to_string()), update_time: Some(Local::now().format(fmt).to_string()), }, BizOrigin { id: new_snowflake_id(), name: "Jecika".to_string(), gender: 0, mobile: "13033457876".to_string(), create_time: Some(Local::now().format(fmt).to_string()), update_time: Some(Local::now().format(fmt).to_string()), }, ]; conn_origin.exec_batch( r"insert into sample (id,name,gender,mobile,create_time,update_time) values (:id,:name,:gender,:mobile,:create,:update)", bizes.iter().map(|p| -> Params { params! { "id"=>p.id, "name"=>p.name.to_owned(), "gender"=>p.gender.to_owned(), "mobile"=>p.mobile.to_owned(), "create"=>p.create_time.as_ref(), "update"=>p.update_time.as_ref() } }), )?; // Let's select payments from database. Type inference should do the trick here. let selected_bizs = conn_origin.query_map( "SELECT id,name,gender,mobile,create_time,update_time from sample", |(id, name, gender, mobile, create_time, update_time)| BizOrigin { id, name, gender, mobile, create_time, update_time, }, )?; println!("selected result {:?}", selected_bizs); Ok(()) }
程式碼並不複雜,首先建立SslOpts,指定CA檔案的位置;然後使用OptsBuilder 生成連結配置資訊;最後建立Connection。後面是執行表建立以及驗證連結,最後是對標的 insert 和 select 操作。
sqlx
sqlx是純 Rust 編寫的非同步 SQL Crate。
-
依賴
[dependencies] # sqlx sqlx = "0.6.2"
-
程式碼
use futures::TryStreamExt; use sqlx::mysql::MySqlPoolOptions; #[tokio::main] async fn main() { let sqlx_opts = sqlx::mysql::MySqlConnectOptions::new() .host("gateway01.us-east-19.prod.aws.tidbcloud.com") .port(4000) .database("test") .username("tidbcloudtier.root") .password("xxxxxxxxxxxx") .ssl_ca("/etc/ssl/cert.pem"); let pool = MySqlPoolOptions::new() .connect_with(sqlx_opts) .await .unwrap(); let mut rows = sqlx::query("select * from sample").fetch(&pool); while let Some(row) = rows.try_next().await.unwrap() { println!("row is {:?}", row); } }
SeaORM
SeaORM是在 sqlx 之上構建的 orm 框架。
-
依賴
[dependencies] # SeaORM sqlx = "0.6.2" sea-orm = { version = "0.10.6", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "macros" ] }
-
程式碼
use sea_orm::ConnectionTrait; use sea_orm::DbBackend; use sea_orm::SqlxMySqlConnector; use sea_orm::{FromQueryResult, Statement as sea_statment}; use sqlx::MySqlPool; #[derive(Debug, FromQueryResult)] pub struct SeaOrmBiz { pub id: i64, pub name: String, pub gender: Option<i8>, pub mobile: String, pub create_time: chrono::NaiveDateTime, pub update_time: chrono::NaiveDateTime, } #[tokio::main] async fn main() { let sqlx_opts = sqlx::mysql::MySqlConnectOptions::new() .host("gateway01.us-east-19.prod.aws.tidbcloud.com") .port(4000) .database("test") .username("tidbcloudtier.root") .password("xxxxxxxxx") .ssl_ca("/etc/ssl/cert.pem"); let pool = MySqlPool::connect_with(sqlx_opts).await.unwrap(); let db = SqlxMySqlConnector::from_sqlx_mysql_pool(pool); let rs = db .execute(sea_statment::from_string( db.get_database_backend(), "select 1 from dual;".to_string(), )) .await; println!(">>>>> Cipher in use from sea_orm:{:?}", rs); let biz: Vec<SeaOrmBiz> = SeaOrmBiz::find_by_statement(sea_statment::from_sql_and_values( DbBackend::MySql, r#"SELECT * FROM sample;"#, vec![], )) .all(&db) .await .unwrap(); println!(">>>>> selet rs is {:?}", biz); }
SeaOrm 依賴 sqlx。首先構建 sqlx::MySqlConnectOptions 然後根據 MySqlConnectOptions 構建 sqlx::MySqlPool 最後構建 sea_orm::SqlxMySqlConnector 用於與 mysql 通訊。
Rbatis
-
依賴
[dependencies] # rbatis integration rbs = "0.1.13" rbatis = "4.0.44" rbdc-mysql = "0.1.18"
-
程式碼
use rbatis::rbdc::datetime::FastDateTime; use rbatis::Rbatis; use rbdc_mysql::options::MySqlConnectOptions; use rbdc_mysql::{driver::MysqlDriver, options::MySqlSslMode as rbdc_MysqlSslMode}; use rbs::to_value; use serde::{Deserialize, Serialize}; use std::collections::HashMap; pub const TABLE_NAME: &str = "sample"; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct BizRbatis { pub id: Option<i64>, pub name: Option<String>, pub gender: Option<u8>, pub mobile: Option<String>, pub create_time: Option<FastDateTime>, pub update_time: Option<FastDateTime>, } rbatis::crud!(BizRbatis {}, TABLE_NAME); #[tokio::main] async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { // rbatis 連線 let rb = Rbatis::new(); let opt = MySqlConnectOptions::new() .host("gateway01.us-east-19.prod.aws.tidbcloud.com") .port(4000) .database("test") .username("tidbcloudtier.root") .password("xxxxxxxxxx") .ssl_mode(rbdc_MysqlSslMode::VerifyIdentity) .ssl_ca("/etc/ssl/cert.pem"); rb.init_opt(MysqlDriver {}, opt).unwrap(); rb.get_pool().unwrap().resize(3); let sql_show_ssl_cipher = "SHOW STATUS LIKE 'Ssl_cipher'"; let cipher_rbatis = rb .fetch_decode::<Vec<HashMap<String, String>>>(sql_show_ssl_cipher, vec![]) .await; println!(">>>>> Cipher in use from rbatis: {:?}", cipher_rbatis); let sql_select_one = format!("select * from {} limit ?;", TABLE_NAME); let row = rb .fetch_decode::<BizRbatis>(&sql_select_one, vec![to_value!(1)]) .await; println!(">>>>> rbatsis select result={:?}", row); Ok(()) }
首先,新建一個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 與 資料庫打交道的更多細節。
咱們下期見。
- 應用健康度隱患刨析解決系列之資料庫時區設定
- 對於Vue3和Ts的心得和思考
- 一文詳解擴散模型:DDPM
- zookeeper的Leader選舉原始碼解析
- 一文帶你搞懂如何優化慢SQL
- 京東金融Android瘦身探索與實踐
- 微前端框架single-spa子應用載入解析
- cookie時效無限延長方案
- 聊聊前端效能指標那些事兒
- Spring竟然可以建立“重複”名稱的bean?—一次專案中存在多個bean名稱重複問題的排查
- 京東金融Android瘦身探索與實踐
- Spring原始碼核心剖析
- 深入淺出RPC服務 | 不同層的網路協議
- 安全測試之探索windows遊戲掃雷
- 關於資料庫分庫分表的一點想法
- 對於Vue3和Ts的心得和思考
- Bitmap、RoaringBitmap原理分析
- 京東小程式CI工具實踐
- 測試用例設計指南
- 當你對 redis 說你中意的女孩是 Mia