MisskeyのTypeORM調査報告書
はじめに
少し前にとあるDiscordでボイスチャットをしたとき、私がMisskeyアドベントカレンダーでFastify v4でActivityPub実装を作る時の注意点、メリット、デメリットについて書くことを少し話したところ、どうにもMisskeyはWebアプリケーションフレームワークのKoaよりデータベースで使っているORMのTypeORMのほうが大きな問題を抱えているみたいな話を聞いたので、軽く調査することにしました。
そんなわけでこの記事は、Misskey Advent Calendar 2022 7日目の記事です。
タイトルが全く違うものになりましたね。すいません。
Misskey 12.119.2
Misskey
ところで皆さんMisskeyはご存知でしょうか。
Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。
あの今話題のMastodonとも繋がることができるActivityPubを実装しています。
暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか?
TypeORM
まあそれはそれとして今回はTypeORMの話をします。
TypeORMは様々なデータベースとプラットフォームをサポートするORMです。
Node.jsのORMとして他にもKnex.jsやPrismaなんかがありますが、他のORMにはない特徴としてActive RecordパターンとData Mapperパターンの両方を実装していて、JavaScript/TypeScriptのデコレーターを使い簡単にデータベースのエンティティリレーションシップモデルを書くことができます。
デコレーターを使えるNestJSとも相性がよく、NestJSのドキュメントにも使い方が記載されています。
CASCADE
まずボイスチャットで聞いた話によるとMisskeyは削除処理がバチクソ重い、特に連鎖削除が重いらしく、SQLではCASCADE文を使い実現している内容になります。
実際MisskeyではTypeORMのonDelete: 'CASCADE'
でALTER TABLE "..." ADD CONSTRAINT "..." FOREIGN KEY ("...") REFERENCES "user"("...") ON DELETE CASCADE ON UPDATE NO ACTION
を追加しているようです。
また、SQLのCASCADE文とは別にcascadeキーを始めとしたCascade処理を行っている処理がTypeORMでいくつか見つかりました。
ただ詳しく調べていくとこれはSQLのCASCADE文と同一の処理をするわけではないとのことで……。なんのこっちゃ。
Migration
リレーションのonDelete, OnUpdateキーは変更時Migrationが必要になるため簡単には変更できないようです。
試しにonDeleteオブジェクトを削除してMigrationしてみましょう。
$ cd misskey/packages/backend
$ NODE_ENV=production yarn build
$ npx typeorm migration:generate -d ormconfig.js -o misskey
こうすればbackendディレクトリにマイグレーションファイルが生成されます。
ALTER TABLE "..." ADD CONSTRAINT "..." FOREIGN KEY ("...") REFERENCES "user"("...") ON DELETE NO ACTION ON UPDATE NO ACTION
CASCADEがNO ACTIONになりました。
ちなみに公式のコントリビューションガイドには異なるコマンドが掲載されています。
このコマンドはYarn v3でなければ動かないそうなので注意が必要です。
$ yarn dlx typeorm migration:generate -d ormconfig.js -o misskey
マイグレーションせずに何とかならないものか。
案
TypeORMのFAQを読み進めていくとこのような記載がありました。
@ManyToOne
や@OneToMany
リレーションでは、@JoinColumn
は不要です。両デコレーターは異なり、@ManyToOne
デコレーターを置いたテーブルにはリレーショナルカラムが存在します。
@JoinColumn
および@JoinTable
デコレータを使用すると、 結合カラム名や結合テーブル名などの結合カラム/結合テーブルの設定を追加で指定することもできます。
www.DeepL.com/Translator(無料版)で翻訳しました。
なので結合カラム名や結合テーブル名などの結合カラム/結合テーブルの設定を追加で指定する必要がない場合は@JoinColumn
デコレーターは必要ないようです。
こちらは@OneToOne
なので@JoinColumn
が必要ですが
こちらは@ManyToOne
なので@JoinColumn
が不要になるかもしれません。
実際に試して連鎖削除を行った際、削除がどの様になるかは不明ですが……。
他にもリレーションのFAQにある自己参照関係や外部キー制約の作成を避ける方法も役に立つかもしれません。
Find
続いて、なぜリレーションを使うのか考えてみましょう。
どうやらsave()
やfind()
でまとめて処理をする時便利なようです。
save()
は全く使われていませんでしたが、find()
は至る所で使われていました。
このfind()
、どうやらバチクソ長いSQL文を生み出す元凶になっているようです。
DISTINCT
皆さんはSELECT DISTINCT "distinctAlias". ...
というバチクソ長いSQL文をみたことありますか?
私はとあるMisskeyサーバーの管理人がこのスロークエリのログをついて言及していたことで知りました。
どうやらこのクエリ、find()
などページングに関するメソッドを使用した時にSELECT ...
と共に発生するクエリのようです。
どうすればよいのでしょうか。
案
上記記事を参照する限りではQueryBuilderのlimit()
とoffset()
を使うことで結合処理が行われていてもDISTINCT無しで処理を行い解決できるようです。
でもQueryBuilderの.take()
や.skip()
、Repositoryの.find({take: ..., skip: ...})
も使いたいですよね?
TypeORMから0.3.0からrelationLoadStrategyキーが追加されているので、これを使えば解決できるかもしれないです。
他にもRepositoryを使わずにQueryBuilderを使えば改善したみたいな内容がissue3857から読み取れます。
TypeORMでデータを取得するときにRepositoryとQueryBuilderどちらを使えばよいのか結構奥が深い話になりそうです。
現在のMisskeyでは圧倒的にRepositoryが使われていました。まあDIも必要なので……。
その他
TypeORMにはデータベースにツリー構造を簡単にセットできる@Tree
デコレーターが用意されています。
階層構造や正規化に困ったら使ってみるのも手かもしれないですね。
階層構造や正規化何もわからん人はSQLアンチパターンを読みましょう。
また、最近@Indexデコレーターを介してマテリアライズドビューのインデックスサポートが追加されたそうです。
@ViewEntity
以外でも使えるかどうかは不明です。uniqueオプションしか使えないみたいですが。
ちなみにMastodonはマテリアライズドビューを使い一部データベース処理を高速化しているそうです。更新時にメモリを結構使うため一長一短あります。
あとは@Column
デコレーターでは様々なオプションを付与できるみたいなので、試しに付与してみるのもいいかもしれません。
他、調査報告書を書くにあたり参考にした記事を紹介して終わりにします。
おわりに
ものすごく疲れたので後は皆さんうまいことやっておいてください。
本番環境ではやらないでね!
やるならGitpodのようなテスト環境でお願いします!!
あれから一年、皆さんいかがお過ごしでしょうか。
Discussion