Slide 1

Slide 1 text

管理のいらない開発用シードを作ってみた 2023/01/25 Rust、何もわからない... #6 koji @fursich0

Slide 2

Slide 2 text

自己紹介 1 ● kojiと呼ばれています(ハンドル名が読めない問題) ● 2022年5月から業務委託としてestieに参画中 ● Rust歴半年ちょっとです ● 休日はわりと登っています fursichです。

Slide 3

Slide 3 text

1. 開発データをサクッと再投入したい 2. シーディング方法の比較 3. 実装方針 4. Rustで実装してみた 目次 2

Slide 4

Slide 4 text

開発シード、どうやって管理してますか?

Slide 5

Slide 5 text

シードのない開発 立ち上げ期、まだ小さなチーム、小さなデータだったころ 開発に必要なデータは各自がポチポチつくっていました。 4

Slide 6

Slide 6 text

シードのない開発 5 productsテーブルのcodeカラムに ユニーク制約を入れました! 制約違反があったら各自データを消して ください! マイグレーションで落ちたのですが どんなSQL流せばいいですか? update room set name = "" where name is null; で一旦動きます〜!

Slide 7

Slide 7 text

シードのない開発 ● 数日ごとに壊れる開発DB ● 秘伝化するセットアップ方法 ● 毎回データを作る手間 さすがにつらい 6

Slide 8

Slide 8 text

シードをつくろう!

Slide 9

Slide 9 text

DB シード登録処理 データ定義 シードの実装は簡単 8

Slide 10

Slide 10 text

DB シード登録処理 データ定義 スキーマは日々変わる 9 スキーマ変更

Slide 11

Slide 11 text

DB シード登録処理 データ定義 シードの管理は簡単ではない 10 壊れる 壊れる スキーマ変更

Slide 12

Slide 12 text

「人気アーティスト」のデータが必要だから、 artists.id=1の持つアルバムの評価を5に変更し ておくよ アプリケーション上のデータの意味も日々かわる 11 artists id name city_id 1 The Beatles 2 2 Eric Clapton 1 3 U2 4 4 Oasis 3 albums id title rating is_new 1 Revolver 4.5 false 2 Definitely Maybe 3.5 true 3 October 3 false 4 Unplugged 5 false 「新着アルバム」画面を出す必要があるから albums.id=2を新着扱いにするよ

Slide 13

Slide 13 text

管理のいらないシードがほしい

Slide 14

Slide 14 text

世の中ではシードをどうやって実装しているのか よくみかける3つのパターン 13

Slide 15

Slide 15 text

データ定義 ①データ・コードをSQLで定義する シード登録処理 + 14 ● プログラマが自由に定義しやすい ● データの関係性が複雑になると可読性・メンテナビリティが悪くなる DB

Slide 16

Slide 16 text

①データ・コードをSQLで定義する create table countries ( id int not null, name varchar(100) not null ); insert into countries (id, name) values (1, 'Afghanistan'); insert into countries (id, name) values (2, 'Albania'); insert into countries (id, name) values (3, 'Algeria'); insert into countries (id, name) values (4, ‘Andorra’); insert into countries (id, name) values (5, ‘Angola’); … create table users ( id int not null, name varchar(256) not null, country_id int not null ); insert into users (id, name, country_id) values (1, 'Bob', 15); insert into users (id, name, country_id) values (2, 'Alice', 33); … 15 一般的にはIDを固定して外部キーを指定するため マジックナンバー管理になり、可読性は低い

Slide 17

Slide 17 text

②データ・コードをプログラムで管理する 構造体(モデル)定義 その永続化を行う アプリケーションコード 16 DB データ定義 インスタンス組み立て + 永続化のためのグルーコード + ● プログラマが自由に定義・カスタマイズしやすい ● 言語機能やDSLを利用できるため、可読性・メンテナビリティは相対的に高い

Slide 18

Slide 18 text

https://github.com/prisma/prisma-examples/blob/latest/typescript/graphql/prisma/seed.ts データ定義 ORMを利用したグルーコード アプリケーション側での モデル定義 17 ライブラリやフレームワークでサポートされるケースが多い

Slide 19

Slide 19 text

DB 登録処理 ③他のDBからデータをダンプ ● データ管理不要 ● センシティブなデータのマスク、アクセス管理が必要 ● 環境別・個人ごとのカスタマイズは困難 本番・staging DB 18

Slide 20

Slide 20 text

まとめ 導入コスト データ管理コスト コード管理コスト SQLで定義 ◎ ✖ ✖ コード定義 ◎ (ライブラリのサポート前提) ▲ 一部アプリケーションコードが 流用可能 ダンプ インフラ側の対応がいる セキュリティ面の検討など ◎ ◎ 19

Slide 21

Slide 21 text

まとめ 導入コスト データ管理コスト コード管理コスト SQLで定義 ◎ ✖ ✖ コード定義 ◎ (ライブラリのサポート前提) ▲ 一部アプリケーションコードが 流用可能 ダンプ インフラ側の対応がいる セキュリティ面の検討など ◎ ◎ 20

Slide 22

Slide 22 text

実装してみた

Slide 23

Slide 23 text

ナイーブに実装する 22 データ定義 インスタンス組み立て + 永続化のためのグルーコード + 構造体定義 永続化を行う アプリケーションコード DB 流用できる

Slide 24

Slide 24 text

見通しが悪くメンテが辛そう・・ 23 let pool = setup_mysql().await?; // userを作成する処理 let user_id1 = User::insert(&pool, &UserInput{ first_name: "Taro".to_string(), last_name: "Yamada".to_string(), email: "[email protected]".to_string(), } ).await?; let user_id2 = User::insert(&pool, &UserInput{ first_name: "Jane".to_string(), last_name: "Conley".to_string(), email: "[email protected]".to_string(), } ).await?; let user_id3 = User::insert(&pool, &UserInput{ first_name: "Jack".to_string(), last_name: "Johnson".to_string(), email: "[email protected]".to_string(), } ).await?; let user_id3 = User::insert(&pool, &UserInput{ first_name: "Foo".to_string(), last_name: "Bar".to_string(), email: "[email protected]".to_string(), } ).await?; // …snip // productを作成する処理 let product_id1 = Product::insert(&pool, &ProductInput { name: "keyboard".to_string(), price: 100000, } ).await?; let product_id2 = Product::insert(&pool, &ProductInput { name: "microphone".to_string(), price: 20000, } ).await?; let product_id3 = Product::insert(&pool, &ProductInput { name: "guitar".to_string(), price: 50000, } ).await?; // dealerを作成する処理 let dealer_id1 = Dealer::insert(&pool, &DealerInput { name: "MacroSoft".to_string(), country: "US".to_string(), contract_type: 3, } ).await?; let dealer_id2 = Dealer::insert(&pool, &DealerInput { name: "April Computer".to_string(), country: "JP".to_string(), contract_type: 2, } ).await?; // 購入記録を作成する処理 let purchase_id1 = Purchase::insert(&pool, &PurchaseInput { buyer_id: user_id2, dealer_id: dealer_id1, product_id: product_id1, amount: 3, discount: None, tax: 14550, net_price: 314550, purchased_at: NaiveDateTime::parse_from_str("2022-02-01 21:23:45", "%Y-%m-%d %H:%M:%S")?, } ).await?; let purchase_id3 = Purchase::insert(&pool, &PurchaseInput { buyer_id: user_id2, dealer_id: dealer_id2, product_id: product_id2, amount: 1, discount: None, tax: 3300, net_price: 23330, purchased_at: NaiveDateTime::parse_from_str("2022-02-01 18:55:20", "%Y-%m-%d %H:%M:%S")?, } ).await?; let purchase_id3 = Purchase::insert(&pool, &PurchaseInput { buyer_id: user_id3, dealer_id: dealer_id1, product_id: product_id2, amount: 2, discount: Some(5000), tax: 3333, net_price: 38888, purchased_at: NaiveDateTime::parse_from_str("2022-08-02 11:20:15", "%Y-%m-%d %H:%M:%S")?, } ).await?; User構造体に新しいenum が生えました! Purchase構造体の責務が 大きすぎるから分割しま す〜

Slide 25

Slide 25 text

開発用途で大きなコードの塊を管理したくない 24 ● コードとデータを分離する ○ スキーマ変更時、なるべくデータの更新だけですむのが理想 ● 実行時に決まるデータを扱う ○ 外部キーのようなidなどの解決 ○ 実行環境ごとに変えたいデータ これらを満たしてくれるシードの仕組みが欲しい!

Slide 26

Slide 26 text

俺の考える最強のシード

Slide 27

Slide 27 text

他言語での例 Railsの提供するテスティングフレームワーク内のfixturesという仕組み ● データだけを分離してYAML管理する ● 外部参照は動的に解決できる https://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures Articleモデルの”first”というラベルがついた レコードを参照する (永続化時に動的に idと置換する) 26

Slide 28

Slide 28 text

実装方針 27 ● yamlファイルから構造体をデシリアライズする ● 動的に決まる参照の解決には、埋め込みタグを利用する

Slide 29

Slide 29 text

シードデータの表現 28 # Companyデータ Denali: name: "有限会社デナリ" FitzRoy: name: "FitzRoy & Company" Estie: name: "株式会社エスティ" # Userデータ Alice: name: Alice email: "[email protected]" company_id: ${{ REF(Denali) }} Bob: name: Bob email: "[email protected]" country_code: 81 company_id: ${{ REF(FitzRoy) }} Dev: name: Developer email: ${{ ENV(DEV_EMAIL:-"[email protected]") }} country_code: 44 company_id: ${{ REF(Estie) }} pub struct Company { pub name: String, } pub struct User { pub name: String, pub email: String, pub country_code: Option, pub company_id: i64, } こんな感じでデータ部分だけを定義したい こういう構造体があったとして・・

Slide 30

Slide 30 text

シードデータの表現 29 # Companyデータ Denali: name: "有限会社デナリ" FitzRoy: name: "FitzRoy & Company" Estie: name: "株式会社エスティ" # Userデータ Alice: name: Alice email: "[email protected]" company_id: ${{ REF(Denali) }} Bob: name: Bob email: "[email protected]" country_code: 81 company_id: ${{ REF(FitzRoy) }} Dev: name: Developer email: ${{ ENV(DEV_EMAIL:-"[email protected]") }} country_code: 44 company_id: ${{ REF(Estie) }} pub struct Company { pub name: String, } pub struct User { pub name: String, pub email: String, pub country_code: Option, pub company_id: i64, } Company保存時に名前とidの対応を保 存しておき、実行時に置換

Slide 31

Slide 31 text

シードデータの表現 30 # Companyデータ Denali: name: "有限会社デナリ" FitzRoy: name: "FitzRoy & Company" Estie: name: "株式会社エスティ" # Userデータ Alice: name: Alice email: "[email protected]" company_id: ${{ REF(Denali) }} Bob: name: Bob email: "[email protected]" country_code: 81 company_id: ${{ REF(FitzRoy) }} Dev: name: Developer email: ${{ ENV(DEV_EMAIL:-"[email protected]") }} country_code: 44 company_id: ${{ REF(Estie) }} pub struct Company { pub name: String, } pub struct User { pub name: String, pub email: String, pub country_code: Option, pub company_id: i64, } 実行時に環境変数と置換 (なければ [email protected]にフォールバック)

Slide 32

Slide 32 text

動的な参照の解決 埋め込みタグはHashMapに名前とidの対応を溜めていくことで単純に置換 YAMLパーサーの前段でプリプロセッサのような処理を行う ● 同名ラベルをもつデータのidを参照できる ● yaml標準のエイリアス利用を利用するアイデアも ● 環境変数の参照をサポート ● デフォルト値を設定した場合、フォールバック可能 31 email: ${{ ENV(DEV_EMAIL:-"[email protected]") }} company_id: ${{ REF(Estie) }}

Slide 33

Slide 33 text

デシリアライズ serde-yamlが利用できそう ● 標準的なデータ仕様にのっかれる ● 表現力が高くあらゆる構造体を表現可能 ● deriveマクロを足すだけで利用可能 32 https://github.com/dtolnay/serde-yaml

Slide 34

Slide 34 text

グルーコード部分のインターフェース 33 // 挿入結果のラベルとidの対応を保持する let mut seeder = DatabaseSeeder::new(); seeder.populate("companies.yml", |input: Company| { // // Companyを挿入する関数 // Fn -> Result })?; seeder.populate("users.yml", |input: User| { // // Userを挿入する関数 // Fn -> Result })?; シード固有ではない共通処理はなるべくまとめ、グルーコード部分を小さく保ちたい 共通する定型処理をまとめたい ● ファイルの読み取り ● 埋め込みタグの解決(置換) ● yamlのデシリアライズ ● 一つ一つのレコードをブロックに渡し ● ラベルとブロックの評価結果を紐付けして保存 ● 後のラベルの解決に使う

Slide 35

Slide 35 text

グルーコード 34 let mut seeder = DatabaseSeeder::new(); // non-asyncブロック(関数)を利用する場合 seeder.populate("users.yml", |input: User| { User::insert(pool, input) })?; let mut seeder = DatabaseSeeder::new(); // asyncブロック(関数)を利用する場合 seeder.populate_async("users.yml", |input: User| { Box::pin(async move { User::insert(pool, input).await }) }) .await?; 非同期関数も扱えるとさらによい

Slide 36

Slide 36 text

実装!

Slide 37

Slide 37 text

ツールとして汎用性がありそう・・? 36

Slide 38

Slide 38 text

OSS化しました

Slide 39

Slide 39 text

cder公開! 38 special thanks to: @kenkoooo, @khei4, @hoyo, @matsu7874 https://github.com/estie-inc/cder

Slide 40

Slide 40 text

解決したいこと ● 依存関係・参照の自動解決 ○ ファイルを一括で読み込んで自動で依存解決できるとオシャレ ○ 今は読み込み順に依存する ○ トポロジカルソートできる(はず) ● asyncブロックにSendを指定するとhigher-ranked lifetime errorになる問題 ● コメントの中にタグを入れるとバグる 39

Slide 41

Slide 41 text

使ってみた感想やPR、ぜひお願いします!! 40