2021年の振り返り
今年は大きな転換の年になったと思うので、年の瀬にログを残します
個人事業主になった
社会人になってからずっと会社に属して働いてきましたが今年から会社をやめ、田舎に戻って個人事業主になりました。仕事は引き続き東京からフルリモートで受けていますが、週5ではなく稼働を落としています。
大きな決断ではありますがなぜ踏み切ったかというと、
- 家族への責任を果たす
- 自身の健康を立て直す
- 仕事だけしていたがために取りこぼしていたいろんなものを拾いなおす
- 技術の仕込み
ということをちゃんとやるために時間や自由度が欲しくなったからです。
家族への責任を果たす
具体的には親の介護的な事情になります。兄に任せっぱなしにしていたのをずっと申し訳なく思っていましたが、これを機にちゃんと向き合うことにしました。田舎に戻ったのはこのためです。
うちの親は億単位の赤字のある会社を引き継いで定年までこつこつ真面目に働き借金を返済しました。しかし安定した資産を築いたがために親類の無心や濫用に巻き込まれ、健康と心を害してからはQoLがダダ下がり。頑張った分もっと報われてほしいと子としては思います。私が誰にも後ろ指をさされることのない充実した教育を受けられたのは何より親の支援あってのことですから、返していこうと。
コロナ禍のせいでできないことも多いですが、できることをしていこうと思っています。
自身の健康を立て直す
睡眠障害でまともなパフォーマンスがでていませんでした。16時出社26時退社みたいな働き方はざらで、会社も上司もかなり融通を効かせてくれましたが治るところまではいきませんでした。(睡眠導入剤は依存性のある麻薬一歩手前ぐらいのやつまでもらいました)
さすがにそんな状況で仕事を続けても誰も幸せにならないだろうということで、仕事よりもまずは健康になることに高い優先順位を置くことにしました。メンタルはもちろん健康診断もよい数値を目指します。数年後にまた仕事のほうにギアを入れる際にしっかり戦える心と身体を少しずつ作り上げていこうと思っています。
今のところ田舎パワーはとても良い感じで効いています。東京にいたころはアトピーのけがあってよく体がかゆくなっていたのですがだいぶ収まっていますし、朝8時半ごろに強烈な朝陽が真横からきて嫌でも生活リズムが整っています。食事も健康的になり、仕事との距離感をかえたこととフルリモートで働いていることからストレスも減りました。この生活を続けていけば問題はないだろうという手ごたえはあります。
仕事だけしていたがために取りこぼしていたいろんなものを拾いなおす
社会人になってからは仕事とゲームしかしていなかったので取りこぼしていたものが多いです。車の免許とかほとんどの人が持ってるのに持ってなかったですからね。さしあたり田舎で生きていくのに必須なので頑張って取りました。
個人事業主になったのを機にお金の知識もつけようと思い日商簿記三級とFP3級の本をぱらぱらと眺めています。資格を取るところまでは今のところ考えてはいませんが青色申告の帳簿ぐらいは理解できるようになろうかなと。その目的では簿記のほうが当たりで学びが多いです。FPのほうは以前生命保険のプロジェクトにいたときに教えてもらった知識とかぶりがあってややつまらない…が通して読んだら印象が変わるかもしれません。
人付き合いまわりも大きな取りこぼし領域だったりしますが相変わらずコミュ障で不慣れです。人から誘われたらほいほいついてくんですが自分から距離を詰められない、詰めようと努力しない傾向にあります。自覚はあるので改善していこうと思っています。というわけで仲良くしましょう。うちの島にきたら若葉マークの車ぐらいは出しますよ!(危険)
技術の仕込み
手持ちの技術を使うだけである程度仕事ができちゃっている現状があり、新しく仕込みをできていないのでお勉強の時間をしっかり取りたいという気持ちがあります。仕事に追われていると今の技術の延長をつまんでいくことはできるんですが、新しいものとなるとなかなか腰をすえて取り組めなかったりします。
以上、いろいろと複合的に考えて会社をやめ個人事業主になったというお話でした。確定申告がんばります。
アプリをリリースした
会社で三年以上作り続けていたものが世にでました。具体的なことは書きませんが、リリース前もリリース後も大変でした。リリース品質には悔いがあり今後改善していきたいと思っています。
振り返ってみてああすれば良かったこうすれば良かったと神の視座で話すのは簡単ですが、渦中にあって常に正確な舵取りを続けるのはほぼ不可能だと思います。その前提にあってもなお学びを語るならば「作るものの規模とコアメンバー数に応じて開発期間は決まり、量産メンバーの増員で開発期間を短縮することはできない(それは品質の悪化を意味する)」というあたりでしょうか。
情報共有(教育というと上から目線感が強いのであまり使いたくない)をしっかりしてコアメンバー相当人数を増やすのも手かと思います。コアメンバーが回っていなかったら無理ですけどね。増員手前でどれだけ準備していけるか。次新規プロジェクトをやるならこのあたりの教訓は生かしたいですね。
このトピックについては書きたいことは無限にあるのですがこのあたりで。
その他のことをつらつらと
趣味
2020年の11月からピアノ(正確には88鍵のMIDIキーボードですが)を弾いていて今年で丸一年になりましたが続いています。そしてピアノの影響をうけてか、ゲームの比重が劇的に落ちました。
もともと作曲というものをしてみたいという気持ちが強くあってDAWのソフトを買ったりしていたのですがさっぱり使いこなせず、音楽理論の本だけ読んで薄っぺらい知識はあるけど作れないというコンプレックスを抱いていました。そもそもまともに音感も育ってない状態で思い通りに曲なんか作れるか!ということで音感を育てるために頻繁に鍵盤に触れる習慣をつけようという意図でピアノを弾き始めたのですが、両手で弾けるようになってからは演奏のほうが楽しくて当初の目的だった音感トレーニングはさっぱりという状況です。さすがに本末転倒なので今年は音感トレーニングの比重を増やしてみようかなとは思っています。曲ひくより退屈なので続くかはわかりませんが、対位法の修行とセットでなんとかいけたりしないかなとぼんやり考えています。
技術
振り返りというより2022年に仕込みたいものです
Unity
主力の飯のタネなのできちんと最新動向を押さえておきたいです。
- Visual Scripting ... プロダクションレベルで使い物になるかを見ておきたい
- Burst Compiler ... 適用可能範囲が広がっているので運用しているアプリに取り込めないか検討してみたい
- ECS ... 最近お気持ち表明が公式からでましたが、Unity2021向けのベータがでたらちょっと触ってみようかな、程度
- Addressables ... 最初期にちょっと触っただけの悪いイメージのままなので一度学びなおしてみてもいいかな、程度
C#/.NET
Unityとならんで主力の飯のタネ。
- 最新のC#動向やAPI ... 性能向上に使えそうなものを検証して運用アプリにじりじりと適用していきたい
- null可能参照型 ... 運用アプリにじりじりと適用していきたい
- EF Core 6 ... EF Core 2.1あたりまでは非常にポンコツ度が高かったのですが、改善を積み重ねて.NETでDBを扱う際のファーストチョイスにしてもよさそうという雰囲気が非常にでています。これは運用アプリに適用するのは難しいですが手持ちの技術としてもっておきたい
- Blazor ... 興味の段階で止まっていますが将来性を感じるので2022年中に一度真面目に手を出せればと思っています
他消化したい積読
- Go言語によるインタープリター入門
- DirectX12関連
さて、つらつらと書いているうちに年が明けました。2022年もよろしくお願いします。
より良いトランザクションスクリプトを目指す
ファウラーのエンタープライズアプリケーションアーキテクチャパターン(PofEAA)において、 ビジネスロジックのアーキテクチャにはドメインモデルかトランザクションスクリプトかという二択があります。
仮にプレイヤーの名前変更(ゲームでは可能なことも普通にあるので)をするとします。
- ドメインモデル
var user = _repository.Find(userId);
user.ChangeName(name); //バリデーションは中で行われる=ビジネスロジックがオブジェクトにある
_repository.Save(user);
- ドメインモデル貧血症
//ロジックがモデルオブジェクトの外にある if(!IsValidName(name)) { throw new ArgumentException("name"); } var user = _repository.Find(userId); user.Name = name; _repository.Save(user);
if(!IsValidName(name)) { throw new ArgumentException("name"); } _dao.UpdateName(userId, name);
daoはData Access Object です。PofEAAでは「テーブルデータゲートウェイ」という名前で紹介されている、SQL文と実行を閉じ込めただけの簡素なデータアクセスクラスです。
SIにいた人なら「ダオ」のほうが通りが良いのではと思います。テーブルデータゲートウェイなんて現場で聞いたことが無い(私がそうというだけかもしれませんが)。
それはさておき、アプリで使われるSQLを完全に制御したい(手書きSQLを使いたい)なら基本的にデータアクセスはO/RマッパーやActiveRecordではなくDAOを選択することになります。
そしてデータアクセスでO/RマッパーやActiveRecordを使わないということはドメインモデルを選択できないということでもあります。
ドメインモデルに実装されるビジネスロジックは、最終的に集約のルートを頂点としたプレーンなオブジェクトグラフを更新するだけで、データの保存に関しては関知しません。O/Rマッパーが、オブジェクトグラフをどう保存するかを知っていてよしなに保存するのです。
DAOが、単に変更後のオブジェクトグラフを渡されてこれを正しく保存せよと言われると大変厳しい。
単に一つのエンティティを保存するだけなら、力業で全プロパティをUpdateする汎用UPDATE文をDAOに用意すれば効率はさておき保存は可能です。しかしオブジェクトがコレクションを持っていたりするともうだいぶ厳しい。
class Blog { ... //これへのAddをDAOで検知するの厳しい public ICollection<Comment> Comments { get; } }
これをスマートに検知できるのはもうシンプルなSQLのラッパーの範疇を超えています。
力技ならCommentDaoに全行InsertOrUpdateみたいなことをできるかもしれないけど、これをやるともうSQLを完全に制御して性能を稼ぐDAOのメリットがありません。O/Rマッパーよりはるかにひどい性能劣化を引き起こしてしまいます。
つまり、SQLを細かく制御したいならばドメインモデルを選択することはできず、トランザクションスクリプト一択だと思います。
なおSQLを細かく制御するということは、「データがどのように保存されるべきか」をビジネス層が知っているということです。少なくとも完全な隠蔽はできません。だからビジネスロジックであるにも関わらず「トランザクションスクリプト」という名前が付けられているのでしょう。
より良いトランザクションスクリプトを目指す
トランザクションスクリプトはアンチパターンではありません。PofEAAでファウラーが「ビジネスロジックが複雑になってきたらオブジェクト指向的な手法のほうが良い」と連呼しているし DDDも有名なのでトランザクションスクリプトはなんとなく肩身が狭いですが、RDBが性能のボトルネックになりうる環境でSQLを細かく制御したいというのは自然な欲求であると自分は思います。 DAOは実装がシンプルなのでオーバーヘッドも少なく、バッチでの使用もまったく問題ありません。O/Rマッパーをバッチで使えと言われたらやったことはないですが個人的にはやや不安だったりします。チェンジトラッカーさん耐えれられるのかな…
というわけで、より良いトランザクションスクリプトのためにモヤモヤと考えていること。
リポジトリパターン
トランザクションスクリプト+DAOではリポジトリパターンは不要と考えます。集約の単位で不変条件を守りながらデータの出し入れをするDDDのデータアクセスレイヤーがリポジトリです。DAOことテーブルデータゲートウェイはテーブル単位なのでリポジトリより細かい単位なので、リポジトリでDAOをラップすれば集約の単位での保存が可能になりますが、おそらくラップしているリポジトリ側で組み合わせ爆発的にメソッドを生やさないといけなくなってくるので、DAOをバラバラに触ったほうが効率が良いでしょう。また、単一エンティティの集約の場合は、リポジトリは単にDAOに引数をリレーするだけの存在になってしまいます。これを維持するのはつらかろう…ということで不要として良いのではと思います。
もしDAOに相当するものを流行りに流されてリポジトリと呼んでいるなら改名したほうが良いでしょう。パターンは同じものを指してこそ。誤用は避けていきたい。
「ふん!リポジトリっていうのかい?贅沢な名前だねぇ…!今からお前の名はダオだ!いいかい、ダオだよ!」
テスタビリティ
I/Oのレイヤーを差し替えてテストしやすくする場合、トランザクションスクリプトが直接DAOを触るので、DAOにinterfaceを持たせて差し替え可能にすることになりますので、DAOのファクトリをDIで挿せばテスタビリティは問題なし。
余談ですが、手書きSQLを使うパターンには行データゲートウェイもあります。これはデータのレコードが自らのCUDを行うオブジェクトです。
public class HogeRecordGateway { //このあたりはDBのカラム public long Id { get; set; } public string Name {get; set; } ... //ここからはCRUDのCUD public void Insert() { ... } public void Update() { ... } public void Delete() { ... } }
テスタビリティを考えると結局はInsert/Update/DeleteはDIされたDAO(テーブルデータゲートウェイ)に任せる必要があるため、行データゲートウェイを生成するファクトリまで必要になってしまうのでイマイチだと思います。
PlainなデータクラスとDAOを使うほうがマシではないかと。
ドメインサービス
DDDにおける「エンティティでもバリューオブジェクトでもない、関数のようにしかモデリングできないもの」がサービスになりますが、これはトランザクションスクリプトでも問題なく使用できます。というかエンティティ側にロジックを持たない以上は「ドメインサービスの組み合わせ+流れるデータ+DAOでの保存=トランザクションスクリプト」という形になってくるはずで、比重が増えます。
CQRS
問題なく使用できますが、コマンド側にもドメインモデルが登場しなくなるのでクエリがドメインモデルをバイパスできるメリットは感じにくくなるかもしれません。キャッシュ参照をクエリ側に閉じ込めることで見通しを良くするメリットは依然として享受できるはずです。(キャッシュ消しはコマンド側からも行う必要があるので、完全に手が切れるわけではありません)
まとめ
スマホゲームのサーバサイドアーキテクチャについてモヤモヤと考えていること
とりとめもなく書き散らかします。
テスタビリティ
真面目に単体テストを書く人がチームに入ったのでテスタビリティについて真面目に考えています。特にインゲームロジックについてはテストを組めるようにすることの価値が非常に大きいので、手間暇かけてテスト可能にしています。
身も蓋もないことをいうと中核のロジック以外はとどのつまりはIOみたいなものなので(ビューやタイマーとかも含む)、IOのレイヤーを差し替え可能にすることがテスタビリティを担保します。動的言語ではレイヤーなどは差し替え自由自在ですが、C#ではこのための最も有力な手段はDIになると思います。
DI考
副作用、寿命、実行時パラメータ、ポリモーフィズム
実製品レベルでDIを組んでみると寿命の問題が難しさの中心にくることがわかります。
ServiceA → ServiceB → ServiceC のような依存がある場合、一番寿命の短いものに全体がひきずられる、ということが良くあります。
public class ServiceA { private ServiceB _serviceB; pubilc ServiceA(ServiceB serviceB) => _serviceB = serviceB; } public class ServiceB { private ServiceC _serviceC; pubilc ServiceA(ServiceC serviceC) => _serviceC = serviceC; } services.AddSingleton<ServiceA>(); services.AddSingleton<ServiceB>(); services.AddTransient<ServiceC>(); //NG: 寿命が短いものが寿命が長いものに使われている!
この手の寿命問題を簡素にするためには、DIコンテナ管理化に置かれるオブジェクトの多くを状態をもたない関数にしてしまうのが最も良いでしょう。状態をもたなければ、TransientだろうがSingletonだろうがそのオブジェクト自体は同じ動作をするので、自分が依存サービスに合わせて自分の寿命をいかようにも落とせます。ステートレスなWebサービス内なら仮に全部TransientにしたとしてもDIに生成されるオブジェクトが1リクエストで何万も生成されるわけないのでallocのコストは無視して良いレベルに収まります。
※リアルタイムなゲームサーバで1サーバマルチインスタンスで毎秒何十フレームも回しているケースではallocのコストも考えたほうがいいとは思います
状態を持たないオブジェクトを主体にすると、ポリモーフィズムの適用範囲もDI管理下のオブジェクト群に寄っていくことになります。
つまり
//サービスのクライアントコード var obj = _factory.Create(type, param); obj.DoPolymorphic(); //objの型に応じた処理
から
//サービスのクライアントコード
_service.DoPolymorphic(type, param);
という、パラメータをコンストラクタで詰めて後で命令だけ出すような可能な限り駆逐されていく方向に向かいます。これはもう必然というか、DIをやってくと前者のようなオブジェクトは使いにくく感じるはずです。DIが、合成のルート(ASP.NET MVCのControllerみたいな)に対して一発Resolveすることで連鎖的に生まれるもろもろは生成の時点では柔軟にパラメータを渡せないことがほとんどなので、パラメータ渡しのためにFactoryを作る羽目になるのが嫌だなぁと、自然になっていくと思います。
生成の重たいサービス
public class HogeController { private readonly IServiceA _serviceA; private readonly IServiceB _serviceB; //コンストラクタは省略 public IAsyncResult DoA() { _serviceA.DoNantoka(); ... } public IAsyncResult DoB() { //DoBが呼ばれたときだけしか使われないサービス //実は生成が重たいので、コンストラクタインジェクションは勿体ない _serviceB.DoNantoka(); ... } }
コメントにかいてあるとおり、コンストラクタで注入はするものの使うかどうかは状況次第の重たいサービスみたいなのがあるとしたら勿体ない。ので、IServiceBを取得するためのブツだけ注入して、状況に応じて使うということが考えられます。
public class HogeController { ... private readonly IServiceBFactory _serviceBFactory; //コンストラクタは省略 ... public IAsyncResult DoB() { var serviceB = _serviceBFactory.Create(); serviceB.DoNantoka(); ... } }
DIの本を読むと、これはアンチパターンらしいです。なんでや!
理由は「Leaky Abstraction」とのことで、解決策として以下のようなコードを上げています。
public class LazyServiceB : IServiceB { private readonly Lazy<ISerivceB> _lazy; public LazyServiceB(IServiceBFactory factory) { _lazy = new Lazy(() => factory.Create()); } public void DoNantoka() { _lazy.Value.DoNantoka(); } }
こいつを挿せばDoB以外ではコスト気にしなくていいでしょうと。なるほどそれ自体はごもっとも。
ただし性能が大事なアプリでコストを隠蔽するのは個人的にはあまり好きではありません。重さの理由がIO待ちだったらfactoryはCreateではなくCreateAsyncを公開するべきだろうし、そういうものを含めた全体で見た場合には必要な情報を露出しているだけで「Leaky Abstraction」とは思えないのですね。ただし後者のLazyな例がハマるケースもあるだろうとは思っています。
実行時パラメータを渡してオブジェクトを生成しなければならない場合はFactory一択ですし、ケースバイケースで考えて使う。
キャッシュとCQRS
Redisのようなキャッシュ層をいれる場合はCQRS(コマンドクエリ責務分離)も採用でいい気がしている昨今です。クエリ専用のDBやテーブルをつくらなくても、コード内でServiceを分けるだけでもだいぶスッキリするかと思います。つまりこう。
public class DeckController { private readonly IDeckQueryService _queryService; private readonly IDeckCommandService _commandService; }
そもそも更新のときには、更新の前の読み取りでキャッシュをつかってはいけないわけですし、根本的に通るコードや関連クラスを分けてしまったほうがスッキリする。クエリはDB or キャッシュのデータアクセスと、クライアントに返すDTOへの変換が主なのでビジネスロジックが関与する余地が少ない。フィルタ要件によっては面倒も多いけど、単純作業が多いだけで複雑ではない。モデルレイヤーを通す必要もないはずです。
コマンド側は複雑なことが多いです。強化であれば、餌になったキャラが消えた結果、セットしていたデッキからは外す必要があり、強化された結果進化しちゃったりすることがあり、ユーザーのパラメータアップによってミッションが達成されることがある。こういうドメインロジックの複雑さはコマンド側に寄ります。決してクエリではない。
コマンド側もキャッシュから読み取ってはいけないがキャッシュを飛ばす必要はあるので、キャッシュと完全にノータッチとはいきませんがそれでも分けたほうが幸せなんじゃないかというのが僕の肌感覚です。ちなみに実戦投入はしていません。
サーバサイドで何をキャッシュするか
ガラケーのブラウザ時代と違ってスマホならばアプリを一度起動すれば自分のデータは自端末にキャッシュされるわけだから、自分のデータの表示のためのRedisキャッシュはあまり意味をなさないのではないかと思います。ギルドやフレンドなどで他人のデータを見る時がRedisキャッシュの一番の用途となる。そう考えると、たとえば他人のデッキ一覧を見る要件は無いが他人が最後に使用した(いわゆる現在の)デッキを見ることはできる要件があるならば、デッキ詳細のオブジェクトグラフをシリアライズしてユーザー単位で格納、とかのほうがキャッシュ効率は良さそうです。ただし、アプリ起動時の一発目にどかっと自キャラ情報をとるところでDBを守らなくて良いのかという気持ちもなくはないので、テーブルをそのまま突っ込むような汎用キャッシュとすぐ手が切れるかと言われればまだ迷いはあります。モヤモヤ
Migrationやデザインタイムの挙動を別アセンブリに閉じ込める
「Migration用のコードがアプリ実行時に含まれるのが嫌だ。ツール実行時だけ存在すればいい」というニーズもごく普通にあろうかと思います。 EF Coreはそのあたりのオプションがちゃんと用意されています。大丈夫だ問題ない。が、結構わかりにくい挙動をしますのでメモを残します。
ソリューション構成は以下とします。
WebApplication1 WebApplication1 (ASP.NET Coreプロジェクト) AppCore (MyDbContextのあるアセンブリ) AppCore.Migrations (MyDbContextのMigrationのコードの生成先)
MyDbContextからパラメータのないコンストラクタを抹殺する
さきにASP.NET固有の問題を片付けます。
ASP.NETからEF Coreを使用する場合、AddDbContextPool<MyDbContext>でPoolを使って性能を稼ぎたいはずですが、Poolを使う場合はDbContextのパラメータのないデフォルトコンストラクタは使用できません。
DBファーストでscaffoldを使ってMyDbContextを作成する場合はデフォルトコンストラクタが勝手に作成されるのでこれを手で消す必要があります。(DbContextOptions<MyDbContext>を受け取るコンストラクタのみとします)
ところが、このデフォルトコンストラクタの削除によってMigrationにエラーがでるようになります。
Migrationを実行するプロジェクト(dotnet ef migrations addを実行するプロジェクト)ではMigrationさんがMyDbContextをnewしようとします。
このとき、ASP.NETのようにpublic static IHostBuilder CreateHostBuilder(string[] args)
が存在するプロジェクトではDI経由でMyDbContextを生成してもらえるので、DbContextOptions引数をもつコンストラクタを読んでもらえるのですが、
Migration実行プロジェクトにCreateHostBuilderがない場合は、MigrationさんはデフォルトコンストラクタでMyDbContextをnewしようとします。Poolのために消しちゃうのでエラーとなります。
この問題を解決するため、Migrationを実行するプロジェクトにCreateHostBuilderがない場合は、IDesignTimeDbContextFactory<TDbContext>
を実装したクラスを作る必要があります。
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; public class DesignTimeTestDbContextFactory : IDesignTimeDbContextFactory<MyDbContext> { public MyDbContext CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>(); optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=testdb;"); return new MyDbContext(optionsBuilder.Options); } }
これでMyDbContext側には必要なコンストラクタのみを残すことができて整います。
また、上記のinterfaceはMicrosoft.EntityFrameworkCore.Design
パッケージにありますが、このパッケージはMigrationを実行するプロジェクトには必ず必要なものです。入れておきましょう。
今回の例ソリューション構成だと、AppCore.Migrationsプロジェクトにパッケージを入れます。
MigrationsAssemblyを指定する
Migrationの既定の動作は、「MigrationファイルはMyDbContextが存在するアセンブリに出力する」というものです。
このルールからそれる場合、オプションでその旨を伝える必要があります。
これは、Migrationを実行するプロジェクトがどこであれ、MyDbContextと異なるアセンブリにMigrationファイルを出力するならば必要です。
public class DesignTimeTestDbContextFactory : IDesignTimeDbContextFactory<MyDbContext> { public MyDbContext CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>(); optionsBuilder.UseSqlServer( "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=testdb;", b => b.MigrationsAssembly("AppCore.Migrations")); return new MyDbContext(optionsBuilder.Options); } }
Migraionファイルが作成されるプロジェクトに、MyDbContextをもつプロジェクトへの参照を持たせる
Migrationで作成されるC#のソースコードはMyDbContextへの参照があるのでコンパイルを通すために単に必要です。 例のソリューション構成だと、AppCore.DesignがAppCoreを参照します。
Migrationファイルが生成されるプロジェクトのアセンブリの出力先を、Migrationを実行するプロジェクトの出力先と揃える
これはわかりにくいですが、仮に今回の例のソリューション構成でAppCoreでMigraionを実行する場合は、
AppCore/bin/$(Configuration)/AppCore.Migrations.dll
が存在するようにAppCore.Migrations側のOutputPathを設定しなければならない、ということです。
この挙動をさせるために、AppCore→AppCore.Migrationsのプロジェクト参照をもたせてしまうと循環参照になるのでNGです。
AppCore.MigrationsプロジェクトでMigrationを実行する場合はこの条件を勝手に満たすので、Migrationファイルが作成されるプロジェクトをMigration実行プロジェクトにしてしまうのが楽です。
もしもMigrations実行プロジェクトとMigrationsファイルが生成されるプロジェクトを分ける場合は、dotnet ef migrationsコマンドでも--projectオプションを渡す必要があります。
例のソリューション構成でAppCoreやWebApplication1でMigrationを実行する場合は以下のようになります。
dotnet ef migrations add MigrationName --project ../AppCore.Migrations
シャドウプロパティ
シャドウプロパティは、エンティティclass側には存在しないがDBのテーブル側には存在する列を作る、使うための機能です。
public partial class Blog { public int Id { get; set; } public string Title { get; set; } public string Text { get; set; } }
こういうclassに対して、Fluent APIでシャドウプロパティを追加で定義できます。
builder.Property(typeof(DateTimeOffset), "Created") .HasColumnType("datetimeoffset"); builder.Property(typeof(DateTimeOffset), "Modified") .HasColumnType("datetimeoffset");
こうすると、EF CoreはCreatedやModifiedをテーブルに列が存在するものとして扱います。
Migrationすればちゃんとテーブルに列が作られますし、select * from Blog
でちゃんとCreated列もModified列も読み込まれますし、ChangeTrackerのAPI経由で値の取得や更新も可能です。
普通にアプリを書いている分にはエンティティclass経由で操作することがほぼ全てだと思われますので、通常のアプリコードからは隠蔽されます。
シャドウプロパティは、他のテーブルへのリレーションのキーとして使われたりするようです。
public partial class Blog { public int Id { get; set; } public string Title { get; set; } public string Text { get; set; } public OtherEntity OtherEntity { get; set; } //シャドウプロパティでOtherEntityId列が隠れている }
システム列として論理削除列、作成日時、更新日時などアプリから隠蔽したい日付を持たせたい場合も有効です。というかMigrationを考えるとシャドウプロパティにするほかありません。
シャドウプロパティに初期値を与える
シャドウプロパティに初期値を与えたい場合、定数であればFluentAPIのHasDefaultValueを使うことができます。
builder.Property(typeof(DateTimeOffset), "Created") .HasColumnType("datetimeoffset") .HasDefaultValue(DateTimeOffset.MinValue);
あるいはSQLのDEFAULT制約を用いることもできます。
builder.Property(typeof(DateTimeOffset), "Created") .HasColumnType("datetimeoffset") .HasDefaultValueSql("getdate()");
コードで動的に値を与えたい場合は、ValueGeneratorを使います。
using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.ValueGeneration; public class CurrentDateTimeOffsetValueGenerator : ValueGenerator<DateTimeOffset> { public override bool GeneratesTemporaryValues => false; public override DateTimeOffset Next([NotNull] EntityEntry entry) { return DateTimeOffset.Now; } }
builder.Property(typeof(DateTimeOffset), "Created") .HasColumnType("datetimeoffset") .HasValueGenerator<CurrentDateTimeOffsetValueGenerator>();
更新時にシャドウプロパティも更新する
基本的に、DB側でTriggerやそれに類する仕組みを使用すべきです。 コードで行う場合、EF Coreにはそれを行う正規の手順はありません。ValueGeneratorは、エンティティの新規作成時のみしか動作しません。
ただしどうしても行いたいなら、DbContext.SaveChanges()をオーバーライドして中でChangeTrackerをごにょることで実現することは可能です。おすすめはしませんが。※非同期の場合はSaveChangesAsyncも
public override int SaveChanges() { var now = DateTimeOffset.Now; foreach(var x in ChangeTracker.Entries() .Where(x => x.State == EntityState.Modified || x.State == EntityState.Added)) { //全てのテーブルにModifiedが存在する前提 x.Property("Modified").CurrentValue = now; } return base.SaveChanges(); }
DB側でTriggerなどを用いて新規作成時や更新時に値をいれる列では、アプリ側で値を入れるのは無駄です。 EF Coreでは、そのようなときに、これはDB側で更新される列だから余計なことをしないように、と伝える方法があります。
builder.Property(typeof(DateTimeOffset), "Created") .HasColumnType("datetimeoffset") .ValueGeneratedOnAdd(); builder.Property(typeof(DateTimeOffset), "Modified") .HasColumnType("datetimeoffset") .ValueGeneratedOnAddOrUpdate();
ValueGeneratedOnAddOrUpdate()というメソッド名をみると、先述したValueGeneratorがUpdate時も動いてくれそうな印象がありますが、これはまったくValueGeneratorとは無関係です。ValueGeneratorはエンティティ新規作成時しか起動されません。
EntityFramework CoreでSELECTせずにUPDATEを行う
O/Rマッパーでデータの更新を行う場合は、
- オブジェクトを取得して
- プロパティを変更して
- SaveChanges()を行う
というのが普通です。
using(var context = new BlogDbContext()) { var blog = context.Blog.Find(1); blog.Title = "まだ〇〇で消耗しているの?"; await context.SaveChangesAsync(); }
取得も更新もしているので、もちろんSELECT文とUPDATE文が飛びます。
「SQLだとPKと変更したい値がわかってるならSELECT無しでUPDATEできるじゃん!無駄!」という気持ちになりますよね。なりませんか。O/Rマッパーは値の変更を追跡しているので、ChangeTrackerに変更を伝える事さえできれば、SELECTは要りません。EntityFrameworkでは、DbContext.Entryメソッドで取得した諸々からChangeTrackerとやり取りをできます。
using(var context = new BlogDbContext()) { var blog = new Blog { Id = 1, Title = "まだ〇〇で消耗しているの?", }; context.Entry(blog).Property(nameof(Blog.Title)).IsModified = true; await context.SaveChangesAsync(); }
このレベルのチューニングをすべきアプリでそもそもなんでO/Rマッパーなんぞ使うんだ…というのはさておきですけどね。
UIElementsの基本構造
UIElementsがいまいち使いにくい。もっと拡張性を上げられないものか…とおもってUIElementsのソースコードをいろいろ読み漁っていたら理解が深まってしまったのでメモとして残します。 読んでも「ふーん」と思うだけできっと役に立ちません。
VisualTreeAssetとは何なのか
var vitualTreeAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Nantoka.uxml");
var rootElement = vitualTreeAsset .CloneTree();
UIElementsを使う場合はいつも上記のようなコードを書くと思います。拡張子がuxmlなのでuxmlファイルの生テキストそのものを読んでいるような気持ちになりますが、これは厳密には違います。
XMLが記述されたテキストファイルであるところのuxmlファイルは専用のScriptedImporterによって前処理され、ScriptableObjectに変換されます。このScriptableObjectがVisualTreeAssetです。つまりVisualTreeAssetはXMLのパースが完了した結果生まれたバイナリです。
VisualTreeAssetに含まれる要素はいくつかの種類に分類されます。
- VisualElementAsset
- inlineStyleSheet
- UsingEntry
- TemplateAsset
などです。順にみていきましょう。
VisualElementAsset
VisualElementAssetは以下のような代物です
[Serializable] class VisualElementAsset { int Id; string 要素名; string フルネーム(解決済み名前空間付き要素名); int 親のId; (string attributeName, string attributeValue)[] 要素がもつ全属性; string 要素内のテキスト; }
VisualTreeAssetにはこれがListで入っています。 この時点では、まだButtonやImageなどの具体的なVIsualElementに解決はされていません。解決のための文字列情報をもっている状態です。
真の姿をみたければ公式のソースを。Propertyまわりがちょっとウッとなります。 https://github.com/Unity-Technologies/UnityCsReference/blob/master/Modules/UIElements/UXML/VisualElementAsset.cs
inlineStyleSheet
UXMLのビジュアル要素には以下のようにstyle属性を書けますが、これを抽出してかき集めたものです。
<engine:Button name="btn" text="Click!" style="height: 40px" />
uxmlファイルと同様にussファイルもScriptedImporterを経由してStyleSheetというScriptableObjectになります。inlineStyleSheetもソースがussファイルじゃなくなっただけでできるものは同じでStyleSheetを作ってVisualTreeAssetのSubAssetとして保持されます。
UsingEntry
UsingEntryは外部のテンプレートへの参照です。
<engine:Template path="Assets/Portrait.uxml" name="Portrait" />
上記のように書けばUXMLでは外部テンプレート(UXML)を参照して名前をつけることができますが、この要素それ自体は何もVisualElementを作りませんのでVisualElementAssetとして保持せずUsingEntryという構造体のListで持っています。
[Serializable] struct UsingEntry { public string alias; public string path; public VisualTreeAsset asset; }
実際に外部Assetへの参照になっているのでAssetBundleの依存解決なんかも上手くいきそうな気がします。というかこうしないと文字列だけではUnityの参照解決の仕組みに乗っかれないので不安というか。
TemplateAsset
TemplateAssetは、UsingEntryで保持した外部テンプレートを使うための仕組みです。
<engine:Template path="Assets/Portrait.uxml" name="Portrait" /> <engine:Instance template="Portrait" /> <engine:Instance template="Portrait" />
上記のようにInstance要素をかけばTemplateを実体化できるわけですが、これは単純なVisualElementとは異なる挙動をする必要がるのでVisualElementAssetだけでは保持されません。TemplateContainerというVisualElementとTemplateAssetの合わせ技で処理されます。
その他のVisualTreeAsset内の要素
Slotsなどあるんですが、まだ私がちゃんと理解していないのでこの記事では触れません。
気になる方は公式のソースを読むなりマニュアルからあたりをつけるなりで平に御容赦を。
IUxmlFactory
IUxmlFactoryはVisualElementAssetをLabelやButtonなどの具体的なVisualElementに変換するための仕組みです。
public interface IUxmlFactory { string uxmlName { get; } string uxmlNamespace { get; } string uxmlQualifiedName { get; } bool canHaveAnyAttribute { get; } IEnumerable<UxmlAttributeDescription> uxmlAttributesDescription { get; } IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription { get; } string substituteForTypeName { get; } string substituteForTypeNamespace { get; } string substituteForTypeQualifiedName { get; } bool AcceptsAttributeBag(IUxmlAttributes bag, CreationContext cc); VisualElement Create(IUxmlAttributes bag, CreationContext cc); }
メンバーは結構ありますが、もっとも大事なのは uxmlQualifiedName とCreateです。
VisualElementAsset内のフルネームとuxmlQualifiedNameが一致したIUxmlFactoryのCreateがVisualElementの生成に使われます。逆にいうとuxmlQualifiedNameが一致するIUxmlFactoryが存在しそのCreateが何らかのVisualElementを返すならば、UXMLにはどんなXML要素を書いても良いのです。これがUXMLの拡張モデルの中核になります。
個人的に胸熱なのが、どうも公式のソースを読む限りUnity2020.1から、IUxmlFactory.Createがnullを返すことが許されるようになる気配があります。これは、UXMLの中に非VisualElement要素を自由に追加できるようになることを意味します。2019.2現在ではUXMLに非VisualElementを混ぜるにはScriptedImporterで特別扱いするしかなく、それためまったくカスタム不可能な領域です。ここに手が入るのは大変好ましいと思います。ただし未来のことなので取り下げられても文句は言えないので鵜呑みにしないでもらえると助かります。
UxmlTraits
IUxmlFactoryは要素とのマッピングを行いますが、UxmlTraitsは属性とのマッピングを行います。VisalElementAsset内の属性のキーと値のペア群を元にしてIUxmlFactory内で生成されるVisualElementのプロパティをセットします。UxmlTraitsは基本的にはIUxmlFactory実装の内部メンバーとして仕事します。
XMLスキーマ生成
Unityのアセットメニューで「Update UIElements Schema」を選択すると、IUxmlFactoryとUxmlTraitsの情報をもとにXMLスキーマが自動生成されます。XMLスキーマはUXMLを書く時のコード補完に使用されますので、ユーザーが追加したカスタムUML要素に対してもコード補完が効くようになります。
なお<engine:Template />や<engine:Instance />にコード補完が効かないのもこの仕組みが理由です。この二つはScriptedImporterで特別扱いされることで機能しているので対応したIUxmlFactoryが存在しないのです。これは2020.1でCreateがnullを返すIUxmlFactoryが作成されることで改善されそうな気配です。
まとめ
UXMLはScriptedImporterによってVisualTreeAssetに変換され、VisualTreeAsset内の各VisualElementAsset要素は対応するIUxmlFactoryによってButtonやLabelなどのVIsualElementに変換されます。 カスタムのIUxmlFactoryを書くことでUXMLを拡張できます。