🤯

MisskeyのTypeORM調査報告書

2022/12/07に公開約6,100字

はじめに

少し前にとあるDiscordでボイスチャットをしたとき、私がMisskeyアドベントカレンダーでFastify v4でActivityPub実装を作る時の注意点、メリット、デメリットについて書くことを少し話したところ、どうにもMisskeyはWebアプリケーションフレームワークのKoaよりデータベースで使っているORMのTypeORMのほうが大きな問題を抱えているみたいな話を聞いたので、軽く調査することにしました。

そんなわけでこの記事は、Misskey Advent Calendar 2022 7日目の記事です。
タイトルが全く違うものになりましたね。すいません。

Misskey 12.119.2

Misskey

https://misskey-hub.net/
https://github.com/misskey-dev/misskey

ところで皆さんMisskeyはご存知でしょうか。
Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。
あの今話題のMastodonとも繋がることができるActivityPubを実装しています。

暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか?

https://join.misskey.page/ja-JP/

TypeORM

https://typeorm.io/
https://github.com/typeorm/typeorm

まあそれはそれとして今回はTypeORMの話をします。
TypeORMは様々なデータベースとプラットフォームをサポートするORMです。
Node.jsのORMとして他にもKnex.jsやPrismaなんかがありますが、他のORMにはない特徴としてActive RecordパターンとData Mapperパターンの両方を実装していて、JavaScript/TypeScriptのデコレーターを使い簡単にデータベースのエンティティリレーションシップモデルを書くことができます。
デコレーターを使えるNestJSとも相性がよく、NestJSのドキュメントにも使い方が記載されています。

https://docs.nestjs.com/techniques/database

CASCADE

まずボイスチャットで聞いた話によるとMisskeyは削除処理がバチクソ重い、特に連鎖削除が重いらしく、SQLではCASCADE文を使い実現している内容になります。
実際MisskeyではTypeORMのonDelete: 'CASCADE'ALTER TABLE "..." ADD CONSTRAINT "..." FOREIGN KEY ("...") REFERENCES "user"("...") ON DELETE CASCADE ON UPDATE NO ACTIONを追加しているようです。

https://sourcegraph.com/search?q=context:global+repo:^github\.com/misskey-dev/misskey%24%4012.119.2+CASCADE&patternType=standard&case=yes&sm=1

また、SQLのCASCADE文とは別にcascadeキーを始めとしたCascade処理を行っている処理がTypeORMでいくつか見つかりました。

https://sourcegraph.com/search?q=context:global+repo:^github\.com/typeorm/typeorm%24+cascade+count:all&patternType=standard&sm=0

ただ詳しく調べていくとこれはSQLのCASCADE文と同一の処理をするわけではないとのことで……。なんのこっちゃ。

https://typeorm.io/relations
https://uyamazak.hatenablog.com/entry/2021/10/06/140909

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になりました。

https://github.com/misskey-dev/misskey/blob/bdbc448d1347b962c703eea8de3fe5fe66625492/CONTRIBUTING.md#L257-L264

ちなみに公式のコントリビューションガイドには異なるコマンドが掲載されています。
このコマンドはYarn v3でなければ動かないそうなので注意が必要です。

$ yarn dlx typeorm migration:generate -d ormconfig.js -o misskey

マイグレーションせずに何とかならないものか。

TypeORMのFAQを読み進めていくとこのような記載がありました。

https://typeorm.io/faq#what-does-owner-side-in-a-relations-mean-or-why-we-need-to-use-joincolumn-and-jointable

@ManyToOne@OneToManyリレーションでは、@JoinColumnは不要です。両デコレーターは異なり、@ManyToOneデコレーターを置いたテーブルにはリレーショナルカラムが存在します。
@JoinColumnおよび@JoinTableデコレータを使用すると、 結合カラム名や結合テーブル名などの結合カラム/結合テーブルの設定を追加で指定することもできます。
www.DeepL.com/Translator(無料版)で翻訳しました。

なので結合カラム名や結合テーブル名などの結合カラム/結合テーブルの設定を追加で指定する必要がない場合は@JoinColumnデコレーターは必要ないようです。

https://sourcegraph.com/search?q=context:global+repo:^github\.com/misskey-dev/misskey%24%4012.119.2+%40OneToOne+AND+%40JoinColumn&patternType=standard&case=yes&sm=1

こちらは@OneToOneなので@JoinColumnが必要ですが

https://sourcegraph.com/search?q=context:global+repo:^github\.com/misskey-dev/misskey%24%4012.119.2+%40ManyToOne+AND+%40JoinColumn&patternType=standard&case=yes&sm=1

こちらは@ManyToOneなので@JoinColumnが不要になるかもしれません。
実際に試して連鎖削除を行った際、削除がどの様になるかは不明ですが……。

他にもリレーションのFAQにある自己参照関係や外部キー制約の作成を避ける方法も役に立つかもしれません。

https://typeorm.io/relations-faq

Find

続いて、なぜリレーションを使うのか考えてみましょう。
どうやらsave()find()でまとめて処理をする時便利なようです。

save()は全く使われていませんでしたが、find()は至る所で使われていました。
このfind()、どうやらバチクソ長いSQL文を生み出す元凶になっているようです。

https://sourcegraph.com/search?q=context:global+repo:^github\.com/misskey-dev/misskey%24%4012.119.2+.find+count:all&patternType=standard&case=yes&sm=0

DISTINCT

皆さんはSELECT DISTINCT "distinctAlias". ...というバチクソ長いSQL文をみたことありますか?
私はとあるMisskeyサーバーの管理人がこのスロークエリのログをついて言及していたことで知りました。
どうやらこのクエリ、find()などページングに関するメソッドを使用した時にSELECT ...と共に発生するクエリのようです。

https://github.com/typeorm/typeorm/issues/3857
https://github.com/typeorm/typeorm/issues/4742
https://kazamori.jp/blogs/2021/07/13/typeorm-distinct-performance/

どうすればよいのでしょうか。

上記記事を参照する限りではQueryBuilderのlimit()offset()を使うことで結合処理が行われていてもDISTINCT無しで処理を行い解決できるようです。
でもQueryBuilderの.take().skip()、Repositoryの.find({take: ..., skip: ...})も使いたいですよね?
TypeORMから0.3.0からrelationLoadStrategyキーが追加されているので、これを使えば解決できるかもしれないです。

https://github.com/typeorm/typeorm/issues/3857#issuecomment-1090377786

他にもRepositoryを使わずにQueryBuilderを使えば改善したみたいな内容がissue3857から読み取れます。
TypeORMでデータを取得するときにRepositoryとQueryBuilderどちらを使えばよいのか結構奥が深い話になりそうです。

現在のMisskeyでは圧倒的にRepositoryが使われていました。まあDIも必要なので……。

https://sourcegraph.com/search?q=context:global+repo:^github\.com/misskey-dev/misskey%24%4012.119.2+QueryBuilder&patternType=standard&case=yes&sm=1

https://sourcegraph.com/search?q=context:global+repo:^github\.com/misskey-dev/misskey%24%4012.119.2+Repository+count:all&patternType=standard&case=yes&sm=1

その他

TypeORMにはデータベースにツリー構造を簡単にセットできる@Treeデコレーターが用意されています。

https://typeorm.io/tree-entities

階層構造や正規化に困ったら使ってみるのも手かもしれないですね。
階層構造や正規化何もわからん人はSQLアンチパターンを読みましょう。

https://www.slideshare.net/t_wada/sql-antipatterns-digest

また、最近@Indexデコレーターを介してマテリアライズドビューのインデックスサポートが追加されたそうです。
@ViewEntity以外でも使えるかどうかは不明です。uniqueオプションしか使えないみたいですが。

https://github.com/typeorm/typeorm/pull/9414

ちなみにMastodonはマテリアライズドビューを使い一部データベース処理を高速化しているそうです。更新時にメモリを結構使うため一長一短あります。

あとは@Columnデコレーターでは様々なオプションを付与できるみたいなので、試しに付与してみるのもいいかもしれません。

https://typeorm.io/entities
https://typeorm.io/decorator-reference

他、調査報告書を書くにあたり参考にした記事を紹介して終わりにします。

https://qiita.com/ericofjp/items/7b1c78edf24fa5ace23d
https://www.ketancho.net/entry/2018/03/07/080000
http://makopi23.blog.fc2.com/blog-entry-177.html

おわりに

ものすごく疲れたので後は皆さんうまいことやっておいてください。
本番環境ではやらないでね!
やるならGitpodのようなテスト環境でお願いします!!

https://zenn.dev/tkithrta/articles/21bb7e49f6941e

あれから一年、皆さんいかがお過ごしでしょうか。

Discussion

ログインするとコメントできます