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

ORM - Object-relational mapping

ORM - Object-relational mapping

2023-03-19 YAPC::Kyoto 2023

Takafumi ONAKA
PRO

March 19, 2023
Tweet

More Decks by Takafumi ONAKA

Other Decks in Technology

Transcript

  1. ORM
    Object-relational mapping
    id:onk
    2023-03-19 YAPC::Kyoto 2023
    1

    View Slide

  2. 自己紹介
    ● 大仲 能史 a.k.a. onk
    ● 芸歴19年 今年大厄
    ● 株式会社はてな チーフエンジニア
    ● Perl歴: 高校〜大学 + はてなの 5 年
    ○ はてな入社以来、Perl プロダクトを担当
    2

    View Slide

  3. 3
    今日の話

    View Slide

  4. 4
    ORM
    Object-relational mapping

    View Slide

  5. 今日の話
    ● ORM概論
    ● ORM導入に当たって考えたこと
    ● ORM導入時の更なる工夫
    ● 今後の展望
    5

    View Slide

  6. 今日の話
    ● ORM概論
    ● ORM導入に当たって考えたこと
    ● ORM導入時の更なる工夫
    ● 今後の展望
    6

    View Slide

  7. 7
    ORM概論

    View Slide

  8. ORM概論
    ● QueryBuilder
    ● Rowへのマッピング
    ● PoEAAの4つのパターン
    8

    View Slide

  9. QueryBuilder
    ● QueryBuilderとは
    ○ SQLを組み立てるためのライブラリ
    ● 代表的なCPANモジュール
    ○ SQL::Maker
    ○ SQL::QueryMaker
    9

    View Slide

  10. QueryBuilder
    ● Queryをパーツから
    組み立てられる
    ● 条件を返すメソッド
    を作っておくと自由
    に組み立てられる
    10
    sql_and(
    sql_in('id', $ids),
    sql_eq('type', 'normal'),
    )
    -- WHERE
    (`id` IN (?, ?, ...))
    AND (`type` = 'normal')

    View Slide

  11. Rowへのマッピング
    ● DBのテーブルの1行を1オブジェクトにする
    ○ fetchrow_arrayref, fetchrow_hashrefして得られた
    値をRowクラスのインスタンスにする
    ● インピーダンスミスマッチの解消
    ○ 関連や、データ制約、トランザクション等をRowでも
    表現していく
    ○ 変換処理を持つ場合も
    ■ datetime のカラムは DateTime obj になるとか
    11

    View Slide

  12. PoEAAの4つのパターン
    ● エンタープライズアプリケー
    ションアーキテクチャパターン
    Patterns of Enterprise
    Application Architecture
    12

    View Slide

  13. PoEAAの4つのパターン
    ● TableDataGateway
    ● RowDataGateway
    ● ActiveRecord
    ● DataMapper
    13

    View Slide

  14. TableDataGateway
    14
    # Read できる
    my $row1 = Gateway->single(id => 1);
    # Create できる
    my $row2 = Gateway->create(id => 2, name => 'Jiro');
    # Update できる
    my Gateway->update(name => 'Sabu', { id => 2 });
    ● CRUDを司る、テーブルと紐付いたクラス

    View Slide

  15. TableDataGateway
    15
    ● CRUDを司る、テーブルと紐付いたクラス
    DB
    Gateway
    Row
    Service ドメイン
    ロジック

    View Slide

  16. RowDataGateway
    16
    my $row = Row->new(id => 2, name => 'Taro');
    # Create できる
    $row->create;
    # Update できる
    $row->update(name => 'Sabu');
    ● Row自身がCRUD操作を知っている

    View Slide

  17. RowDataGateway
    17
    ● Row自身がCRUD操作を知っている
    DB
    Row
    Service ドメイン
    ロジック

    View Slide

  18. ActiveRecord
    18
    ● クラス自身がTableDataGatwayである
    ● インスタンスがRowで、自分自身のCRUDを
    知っているRowDataGatewayである
    DB
    Model
    Service
    ドメイン
    ロジック

    View Slide

  19. ActiveRecord
    19
    ● Modelがドメインロジックを持つ
    ○ RowDataGatewayはあくまでデータの入出力を担当
    ○ 例えば関連レコードの読み込みや更新のような、複数
    テーブルにまたがるメソッドを持つとか
    DB
    Model
    Service
    ドメイン
    ロジック

    View Slide

  20. DataMapper
    20
    ● Modelはドメインモデル
    ○ TableDataGatewayと違い、ModelはRowではない
    ○ テーブル構成とまったく違うModelがあり得る
    DB
    DataMapper
    Model
    Service
    ドメイン
    ロジック

    View Slide

  21. ORM概論 > PoEAAの4つのパターン
    21
    パターン名 永続化層とModelが ドメインロジック置き場
    TableDataGateway 別クラス トランザクションスクリプト
    RowDataGateway 同じクラス トランザクションスクリプト
    ActiveRecord 同じクラス ドメインモデル
    DataMapper 別クラス ドメインモデル

    View Slide

  22. 今日の話
    ● ORM概論
    ● ORM導入に当たって考えたこと
    ● ORM導入時の更なる工夫
    ● 今後の展望
    22

    View Slide

  23. ORM導入に当たって考えたこと
    ● ORMのCPANモジュール
    ● はてなでよくあるアーキテクチャ
    ● ギャップ少なく導入する
    23

    View Slide

  24. ORMのCPANモジュール
    ● DBIx::Class
    ○ たぶんデファクト
    ○ カヤックさん方面でよく聞く
    ● Teng
    ○ YAPCで無限に聞いていたので馴染みがある
    ● Aniki
    ○ DeNAさん、モバファクさん方面で聞いていた
    24

    View Slide

  25. はてなでよくあるアーキテクチャ
    ● Service層がビジネスロジックを持つ
    ● Service層がTableDataGatewayでもある
    ● ModelはClass::Accessor::Lite
    ○ Associationは無くイミュータブルとして扱っている
    25

    View Slide

  26. はてなでよくあるアーキテクチャ
    26
    package Sample::Service::User;
    sub change_profile {
    my ($class, $id, $name) = @_;
    my $user = $class->find_by_id(db => $db, id => $id);
    return unless $user;
    return unless $class->validate_name($name);
    $class->update_user(
    db => $db,
    user => $user,
    params => { name => $name },
    );
    }

    View Slide

  27. はてなでよくあるアーキテクチャ
    27
    package Sample::Service::User;
    # Read
    sub find_by_id {
    my ($class, $db, $id) = @_;
    return $db->select_row_as(q[
    SELECT * FROM user
    WHERE id = :id
    ],
    { id => $id },
    'Sample::Model::User'
    );
    }

    View Slide

  28. はてなでよくあるアーキテクチャ
    28
    package Sample::Service::User;
    # Update
    sub update_user {
    my ($class, $db, $user, $params) = @_;
    # 変更がなかったら更新しない
    return if (
    $user->name eq $params->{name} &&
    $user->type eq $params->{type}
    );
    $db->query(q[
    UPDATE user
    SET name = :name,
    type = :type
    WHERE id = :id
    ], {
    name => $params->{name},
    type => $params->{type},
    id => $user->id,
    });
    }

    View Slide

  29. はてなでよくあるアーキテクチャ
    ● 薄いフレームワーク
    ○ テーブル単位のModelやService
    ● DBIx::SunnyやDBIx::Handler::Sunny
    ○ SQLを書く
    29

    View Slide

  30. ギャップ少なく導入する
    ● TableDataGatewayとして使っている
    ○ ドメインロジックはServiceにある
    ● RowはImmutable
    ○ 関連を持っていない
    ● 挙げた3つだとTengが一番近い
    30

    View Slide

  31. TengとPoEAAの4分類
    ● TableDataGateway
    ○ テーブル名を文字列で指定するので怪しいがまぁYes
    ● RowDataGateway
    ○ Row自身がCRUDを知っているのでYes
    ● ActiveRecord
    ○ ではない
    ● DataMapper
    ○ として実装することはやればできるがRowが無駄
    31

    View Slide

  32. Tengの場合の差分
    32
    package Sample::Service::User;
    sub change_profile {
    my ($class, $id, $name) = @_;
    my $user = $class->find_by_id(db => $db, id => $id);
    return unless $user
    return unless $class->validate_name($name);
    $class->update_user(
    db => $db,
    user => $user,
    params => { name => $name },
    );
    }

    View Slide

  33. Tengの場合の差分
    33
    package Sample::Service::User;
    # Read
    sub find_by_id {
    my ($class, $db, $id) = @_;
    - return $db->select_row_as(q[
    - SELECT * FROM user
    - WHERE id = :id
    - ],
    - { id => $id },
    - 'Sample::Model::User'
    - );
    + return $teng->single('user', { id => $id });
    }

    View Slide

  34. Tengの場合の差分
    34
    package Sample::Service::User;
    # Update
    sub update_user {
    my ($class, $db, $user, $params) = @_;
    # 変更がなかったら更新しない
    return if (
    $user->name eq $params->{name} &&
    $user->type eq $params->{type}
    );
    - $db->query(q[
    - UPDATE user
    - SET name = :name
    - SET type = :type
    - WHERE id = :id
    - ], {
    - name => $params->{name},
    - type => $params->{type},
    - id => $user->id,
    - });
    + $teng->update('user', $params, { id =>
    $user->id });
    }

    View Slide

  35. 今日の話
    ● ORM概論
    ● ORM導入に当たって考えたこと
    ● ORM導入時の更なる工夫
    ● 今後の展望
    35

    View Slide

  36. ORM導入時の更なる工夫
    ● Greppabilityの確保
    ● AbstractRepositoryの導入
    ● AbstractRowの用意
    ● プラグインで拡張していく
    36

    View Slide

  37. Greppabilityの確保
    ● 今まではテーブルに対する操作を一覧できた
    ○ FROM userやUPDATE userで検索
    ● Tengのテーブル名文字列指定だと検索不能
    ● Repository層を導入する
    37

    View Slide

  38. Greppabilityの確保
    38
    package Sample::Service::User;
    # Read
    sub find_by_id {
    my ($class, $db, $id) = @_;
    - return $teng->single('user', { id => $id });
    + return Sample::Repository::User->single({ id => $id });
    }

    View Slide

  39. Greppabilityの確保
    ● RepositoryがTableDataGatewayそのもの
    ● Repository名で検索で全ての操作が分かる
    ● RowDataGatewayとしてのTengは封印する
    39
    DB
    Gateway
    Row
    Service ドメイン
    ロジック

    View Slide

  40. AbstractRepositoryの導入
    ● Repo::User->singleのように扱いたい
    ○ クラスメソッドとしてTengのCRUDメソッドを呼ぶ
    ○ 全テーブルのRepositoryの親クラスで実装
    ● 単純なCRUDの他に用意したもの
    ○ count, bulk_insert, search_with_pager
    40

    View Slide

  41. AbstractRowの導入
    ● すべてのRowの親クラス
    ● いくつか共通のメソッドを置いた
    ○ new_from_hash
    ■ テスト時とかに便利
    ○ bless_class_accessor_lite
    ■ 今までのCALのRowクラスとの相互運用のため
    ○ will_changes
    ■ いわゆるdirty(is_changed)
    ■ これから$paramsに変更するとdirtyになるか?を返す
    41

    View Slide

  42. プラグインで拡張していく
    ● RepositoryとRowの親クラスができた
    ● どんどん拡張できる
    ○ search_in_period
    ■ WHERE start_at <= :now AND :now < end_at
    ■ $row->in_period #=> Bool
    ○ insert_or_update
    ■ なければinsert、あればupdateを正しくやる
    42

    View Slide

  43. 今日の話
    ● ORM概論
    ● ORM導入に当たって考えたこと
    ● ORM導入時の更なる工夫
    ● 今後の展望
    43

    View Slide

  44. 今後の展望
    ● GraphQLを導入している
    ○ DataLoaderを使ってPKで引くだけのものが増える
    ○ Mutationはそのものがトランザクションスクリプト
    ○ RESTに比べ、Fat Resolverでも破綻しづらい
    ○ Service層は更に薄くなるだろう
    44

    View Slide

  45. 今後の展望
    ● Rowにビジネスロジックを寄せていく?
    ○ 僕はドメインモデル大好きなので雄弁であって欲しい
    ○ 薄くなるService層とのバランスで、今後必要になる
    ことが増えるかどうかが分からない
    45

    View Slide

  46. 今後の展望
    ● RowDataGatewayを解禁する?
    ○ ビジネスロジックをRowに持たせやすくなる
    ○ setterやcallbackは使いこなすとものすごく便利
    ○ greppabilityが減る
    ■ ロジックを追いづらくなる
    ■ 型の恩恵を強く受けるIDEがあれば解決するかも
    46

    View Slide

  47. 今後の展望
    ● Associationを解禁する?
    ○ Row同士で関連を辿れるようにするかどうか
    ■ Tengは対応していない、Anikiは対応している
    ■ ギャップ少なく導入するためにTengにしたが……
    ○ N+1は発生しやすくなる
    ○ DataLoaderで収まる世界だと重要性が低いかも
    47

    View Slide

  48. 48
    まとめ

    View Slide

  49. まとめ
    49
    ● ORMを考えるとアーキテクチャに行き着く
    ○ 何をドメインモデルにするのか。Row?Service?
    ○ 責任境界は何か
    ○ どういう層を用意すると強制できるか
    ○ PoEAAのように20年前にパターン化されている
    ■ 温故知新

    View Slide

  50. まとめ
    50
    ● ORM導入でアーキテクチャが分かりやすく
    ○ 重厚なServiceを2つの役割に分割した
    ■ TableDataGatewayとしてのTeng
    ■ トランザクションスクリプトを書くService
    ● 導入の痛みは少ない
    ○ 単純に移行するなら逐次変換でいける
    ○ ORMっぽいコードに変換するならもう一手間加える

    View Slide

  51. まとめ
    51
    ● ORMっぽいコードとは
    ○ 共通化による加速
    ■ 同じことを同じように行う
    ■ 拡張ポイントが分かりやすく、プラグインを書く圧がある
    ○ ORMに任せるとコード量が激減する
    ■ 普通のことは既に実装されている
    ■ 普通じゃない=複雑なところだけコードを書いていく

    View Slide