Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rustで管理しない開発用データの作り方

 Rustで管理しない開発用データの作り方

登壇資料

イベント
Rust、何もわからない... #6
https://estie.connpass.com/event/270226/

紹介したライブラリ
https://crates.io/crates/cder
https://github.com/estie-inc/cder

Koji Onishi

January 25, 2023
Tweet

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. シードをつくろう!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  12. 「人気アーティスト」のデータが必要だから、
    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を新着扱いにするよ

    View Slide

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

    View Slide

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

    View Slide

  15. データ定義
    ①データ・コードをSQLで定義する
    シード登録処理

    14
    ● プログラマが自由に定義しやすい
    ● データの関係性が複雑になると可読性・メンテナビリティが悪くなる
    DB

    View Slide

  16. ①データ・コードを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を固定して外部キーを指定するため
    マジックナンバー管理になり、可読性は低い

    View Slide

  17. ②データ・コードをプログラムで管理する
    構造体(モデル)定義
    その永続化を行う
    アプリケーションコード
    16
    DB
    データ定義
    インスタンス組み立て

    永続化のためのグルーコード

    ● プログラマが自由に定義・カスタマイズしやすい
    ● 言語機能やDSLを利用できるため、可読性・メンテナビリティは相対的に高い

    View Slide

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

    View Slide

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

    View Slide

  20. まとめ
    導入コスト データ管理コスト コード管理コスト
    SQLで定義 ◎ ✖ ✖
    コード定義

    (ライブラリのサポート前提)

    一部アプリケーションコードが
    流用可能
    ダンプ
    インフラ側の対応がいる
    セキュリティ面の検討など
    ◎ ◎
    19

    View Slide

  21. まとめ
    導入コスト データ管理コスト コード管理コスト
    SQLで定義 ◎ ✖ ✖
    コード定義

    (ライブラリのサポート前提)

    一部アプリケーションコードが
    流用可能
    ダンプ
    インフラ側の対応がいる
    セキュリティ面の検討など
    ◎ ◎
    20

    View Slide

  22. 実装してみた

    View Slide

  23. ナイーブに実装する
    22
    データ定義
    インスタンス組み立て

    永続化のためのグルーコード

    構造体定義
    永続化を行う
    アプリケーションコード
    DB
    流用できる

    View Slide

  24. 見通しが悪くメンテが辛そう・・
    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構造体の責務が
    大きすぎるから分割しま
    す〜

    View Slide

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

    View Slide

  26. 俺の考える最強のシード

    View Slide

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

    View Slide

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

    View Slide

  29. シードデータの表現
    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,
    }
    こんな感じでデータ部分だけを定義したい
    こういう構造体があったとして・・

    View Slide

  30. シードデータの表現
    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の対応を保
    存しておき、実行時に置換

    View Slide

  31. シードデータの表現
    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]にフォールバック)

    View Slide

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

    View Slide

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

    View Slide

  34. グルーコード部分のインターフェース
    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のデシリアライズ
    ● 一つ一つのレコードをブロックに渡し
    ● ラベルとブロックの評価結果を紐付けして保存
    ● 後のラベルの解決に使う

    View Slide

  35. グルーコード
    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?;
    非同期関数も扱えるとさらによい

    View Slide

  36. 実装!

    View Slide

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

    View Slide

  38. OSS化しました

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide