Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

3 今日の話

Slide 4

Slide 4 text

4 ORM Object-relational mapping

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

7 ORM概論

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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を司る、テーブルと紐付いたクラス

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

はてなでよくあるアーキテクチャ 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 }, ); }

Slide 27

Slide 27 text

はてなでよくあるアーキテクチャ 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' ); }

Slide 28

Slide 28 text

はてなでよくあるアーキテクチャ 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, }); }

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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 }, ); }

Slide 33

Slide 33 text

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 }); }

Slide 34

Slide 34 text

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 }); }

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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 }); }

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

48 まとめ

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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