Training in Commercial Pilotage with FTD - The Best Take Off Climb

都内某所で Flight Simulator による F/O 訓練。B737-800 で 2.0H。

Acceleration / Deceleration

本日の訓練内容は Acceleration / Deceleration (増減速)。前半戦は初対面の K 教官。前回の Straight & Level Check の完了後、その感覚を忘れないように訓練頻度を高めようと 3 週間おきに 2 回の予約をしていたのだが、体調不良で 1 回欠席し、結局 1 ヵ月半ぶりの訓練になる。この間、イメージトレーニングもほとんどしていなかったのだが、今回、操縦し始めてみると自分でも驚くくらい、滑らかに安定してコントールできている。高度・速度調整も細かく、旋回しながらでも滑らかにこなす。

6,000 ft に上がったところで Acceleration / Deceleration をやってみましょうということで、これまで 240 kt 一定だったところを 200 ~ 280 kt で変化させてみる。A/S・Alt・Power の相互変換と Pitch との関係などはきちんと理解しており、正しい方向に対応できるのだが、やってみて難しいのはやはり Power 変量やリードの適切な設定がわからないということ。教官から助言してもらったのは、A/S ゲージに表示されている、グリーンの矢印 (= A/S 変化率) の先がマゼンタのバグ (= A/S 目標値) に近づいたら Power を「じわじわ」と戻す、ということ。これがまさしく正鵠。この方法を実践すれば困ることがない。

教官から「これだけうまく操縦できるのなら Straight & Level や Acceleration / Deceleration だけではもったいない、いろいろやってみましょう」と言われ、RJTT RWY 34R から Take Off → Landing をすることになる。ここで FD (Flight Director) なるもの *1 を使ってみる。アナログの ILS 指示器は 1 ~ 2 度使ったことがあるが、FD を利用するのは初めて。十字となっているところに ADI の中心点をもってくればよい、ということでそのようにしてみるが ... 大きな操作を大して必要としない Traffic Pattern を飛んでいるはずなのに FD があっちへ行けこっちへ行けと上下左右に激しく動き回り、外の景色もグワングワン揺れている! たまらず「なぜこんなに激しく動き回るのか?」と教官に聞いてみると「十字を追従する操作が激しいから」だと言う。なるほど、十字と中心点との位置合わせに夢中になってしまうと、目標コースに対し機位は合っても運動量が合わず Overshoot することになるわけで、目標コースへ徐々に Intercept することを心掛け、滑らかに十字追従すればよいと理解した後は、Smooth Flight になる。3,000 ft, 165 kt の低高度・低速で飛行を続け、そうこうするうちに RWY 34R Short Final で PAPI が見えてくる。最後 Power を絞るタイミングを教官に Cue 出ししてもらったものの、 ジェット機で初めての Smooth Landing。あまりに Center Aligning だったため、次に Take Off するときに設定初期化済みだと勘違いしてしまったほど。

1 時間たったところで教官交代して後半戦、また Acceleration / Deceleration の訓練。RJTT RWY 34R Take Off からいつものように 6,000 ft, 240 kt を目指す。過去最高の Smooth Climb & Level Off で 5,000 ft から 1 分 23 秒で 6,000 ft ± 20 ft, 240 kt ± 0.4 kt, Heading + 1°にて Stabilize する *2 ... のだが、教官が「Stabilize したらコールしてください」と言うため、ここから Deviation を潰さなければならないのかと考えるも集中力がなくなり、時間が経つうちに Deviation が ± 60 ft、± 2 kt と却って広がってしまう。敗因は Deviation を潰そうと意識しすぎて維持目標を Pitch から VSI に移してしまったこと。VSI は結果として動くものであり、変化の初動と方向を捉えるのには向いているが、遅行指標である VSI を目標とすると Delayed Feedback に基づいて操作することになり Overshoot するという基本をうっかり忘れてしまう。

集中力は切れたものの、教官からは「現役パイロットと勝負できるくらい繊細なコントロールができている」との評価をいただき、Acceleration / Deceleration へ進む。後半戦をやってわかったことは、Acceleration よりも Deceleration の方が仕上げるのが難しいということ。Climb よりも Descent の方が Level Off するのに難しいことに似ている。低速はコントロールが効きが悪いこと (加えて、ジェットはレシプロに比べてエンジンのレイテンシーが長く、諸元変更時の反応が悪いこと) に起因している。逆に高速は Pitch のわずかなズレが Altitude の大きなズレを招く。 減速・低速・降下時は仕上げに、高速時は Pitch に特に意識を向ける必要がある。

また、Power Setting のために EICAS に視線を向けると Pitch が大きく狂うのが弱点。克服するには、Throttle レバーをどれだけ動かせば ΔN1 = 1% か感覚を掴み、EICAS は水準を確認するためにちらっと一瞥するにとどめる必要がある。さきほどの Climb to and Level Off at 6,000 ft の時と同じく、VSI ではなく、目標を定めて Pitch を固定することが重要。Scanning の 70% は Pitch であるべき、とは教官談。たしかに Pitch を一定枠内に固定しておきさえすれば A/S, Altitude, Power の修正量は簡単に把握でき、また、微修正で済む。

その後、Constant Rate Climbing / Descent, Constant Speed Climb / Descent 等 (いま Guam の Instrument Flying コースにて不定期で実機訓練している) 科目に取り組む。教官から矢継ぎ早に指示出し。その中で、6,000 ft から 7,000 ft に上昇しましょう ... (心の声:はいはい、いつものとおり Throttle を 7 - 8% Up と) ... Climbing Rate は 500 ft/min で ... (Constant Rate Climb ですな、500 ft/min なら穏やかな Alt Transition、適切な Pitch を穏やかに探って) ... その間に 260 kt から 220 kt へ落としましょう ... (なに!? Throttle を戻す、いや、かなり絞らないと) ... なんて応用問題も。

得たインサイトが多く、また、自身の技量向上をはっきりと自覚した有意義な訓練であった。

本日の重要ポイント

  • Scanning の 70% は Pitch
  • (Pitch も FD も) ゆっくり動かし、じっと耐えて安定させ、必要変量を見極める
    • VSI を目標にすると Delayed Feedback により Overshoot する
  • Throttle レバーの必要操作量を手感覚で掴む
    • 1 kt = 20ft = ΔN1 1% (戻し操作必要) のトレードオフと合わせて覚える
  • 増減速時の Pitch 変化、Yoke の Feedback は徐々に変化していく
  • 増減速の仕上げでは、グリーンの矢印 (= A/S 変化率) の先がマゼンタのバグ (= A/S 目標値) に近づいたら Power をじわじわと戻す
  • 減速・低速・降下時は仕上げに、増速・高速・上昇時は Pitch に特に意識を向ける

*1:Flight Director : PFD (Primary Flight Display) 上で、目標とすべき Pitch, Bank をマゼンタの十字 FD Command Bars で示すもの。

*2:スクールの Allowance は Altitude ± 20 ft, A/S ± 1 kt, Heading ± 1°。

Training in Commercial Pilotage with FTD

都内某所で Flight Simulator による F/O 訓練。B737-800 で 2.0H。

Straight & Level Flight

いつものとおり Take Off from RJTT RWY 34R から 6,000 ft, 240 kts での Straight & Level Flight。

この一か月は多忙でイメージトレーニングできず、また本日は体調が悪いため、ステップチェックの水準には至らないと N 教官に予告しておく。「では、1時間だけいつもの通りの訓練をしたら、羽田・成田でトラフィックパターンでもしますか」と提案されるも、新しいことを始めたら操縦の感覚が狂うような気がして逡巡する。いつものメニューをだらだらとして過ごすのが今日のところはベストということにしておく。

K 教官と 1 時間 15 分ほどいつものメニューを基本としつつ、高度を 6,000 ft, 7,000 ft, 8,000 ft の間で変えてみたり、240 kts を 220 kts にしてみたり、重量を重くしてみたり、オートパイロットにして諸元修正のリードをモニターしてみたり、と少しだけ逸脱してみる。そのおかけでいろいろと発見あり。「今日は収穫があったな~、後でビデオを見て復習し、次回のステップチェックに備えよう」と思った矢先 ... N 教官から「さて、ではステップチェックしましょう、K 教官からエンドースが出ました」と。いやいやいや、今日はムリでしょうと思いつつも、まあダメもとでやってみる。

Take Off から 5,000 ft までは、Heading Deviation もなくきれいに安定して上昇する。5,500 ft から 6,000 ft までは、Power の適切なリードがいまひとつわからず 60 - 68 % くらいのレンジで上げたり下げたりして VSI が大きく揺らぐ。さきほど撮影したオートパイロットのリードを参考にして復習してからチェックに臨もうとしていたくらいであるため、Transition がまるで設計できていない。

5,000 ft 超えて 2 分以内に 6,000 ft で Stabilize、そのまま 2 分維持という目標に対し、x 分 xx 秒 *1 で Stabilize @ 6,000 ± 5 ft、240 ± 0.1 kts, Heading 337 ± 0。不合格かと思いきや「合格者の平均タイムの半分で優秀だった」と合格。体調が悪く今日はないなと思っていた Straight & Level Flight のステップチェックをクリアしてしまった。

本日の重要ポイント

  • VSI の針が Neutral Position にまとわりつくよう (= 針 1 本以内) に Deviation を抑える
    • ただし、操作の対象は Pitch であり、VSI を目標にしてはならない
  • Pitch 維持・調整には、ADI の中心点だけではなく Model Plane の Wing の上辺・下辺も含め近隣の Gauge の上辺・下辺に最も近い部分を効果的に利用する
  • Constant Rate Climb / Descend のスキャンは、ADI の Pitch ⇔ VSI (500 ft/min 未満は針、500 ft/min 以上は下辺の数値) の間を行き来する
  • 重量増加、低速の場合の安定姿勢はより Pitch Up になる、安定する Pitch を早めにつかむ
  • オートパイロットによる Transition は2段階で Intercept する
    • VSI レートが高いと目標高度への Intercept が難しいため、マニュアル操縦も 2 段階調整による Intercept をするとよい
    • あらかじめ、± 1,000 - 2,000 ft @ 500 ft/min などの穏やかな Transition のレートやリードを憶えておき、1 段階目はこのレートへ落とすことを目標にする

*1:ネタバレになってしまうため、タイムとクライテリアは非公開とする。

Functional Programming w/ C# LINQ

Python は好きになれない。

Python の長所と言われているものは、プログラミング初心者相手のごまかしであるか、あるいは C などの古い言語に対するアドバンテージであり、ライバルとなる他のモダン言語に対するものではない。オフサイドルールも弊害が大きくて嫌いであるが、一番の嫌いな点は、列挙 (Enumeration) の論理構成が直感に反し、思考がいちいち妨げられることだ。思考の順序と異なるために読みにくいし、書くときはカーソルを右に左に大きく動かさなければならない。

列挙の扱い方を C# (LINQ メソッドチェーン) と Python とで比較してみる。

1 から 10 までの整数のうち、偶数には 'Even', 奇数には 'Odd' を付してタプルを返す
  // C#
  Enumerable.Range(1, 10)
    .Select( x => x % 2 == 0 ? (x, "Even") : (x, "Odd") );

偶数なら 'Even'、そうでないなら 'Odd'。条件式 → 結果1 → 結果2 と並ぶ。

  # Python
  [ (x, 'Even') if x % 2 == 0 else (x, 'Odd') for x in range(1, 11) ]

'Even' を返すよ、偶数なら、そうでないなら 'Odd'。結果1 → 条件式 → 結果2と並ぶのは非常にみづらい。
なぜ、条件式を結果ではさむのか。

1 から 10 までの整数のうち、偶数のみを2乗して返す
  // C#
  Enumarable.Range(1, 10)
    .Where( x => x % 2 == 0 )
      .Select( x => x * x );

1 から 10 を対象として、偶数に絞り、2乗する。お題の指示の順序の通り。

  # Python
  [ x * x for x in range(1, 11) if x % 2 == 0 ]

2乗を返すよ、1 から 10 を対象として、ただし偶数のみね。うーん。なぜ、列挙を map と filter ではさむのか。
2乗を返すよ、から始めるならせめて、2乗を返すよ (map)、偶数のみね (filter)、1 から 10 を対象として (enumerate)、にならないものか。

ジャグ配列をフラット化する、フラット化して各要素を2乗する
  // C#
  var arr = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ];

  arr.SelectMany( i => i );                      // フラット化
  arr.SelectMany( i => i.Select( j => j * j ) ); // フラット化して各要素を2乗

SelectMany で要素を掴む i が IEnumerable<int> 型であることを意識すれば難しくない。

  # Python
  arr = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]

  [     j for i in arr for j in i ] # フラット化
  [ j * j for i in arr for j in i ] # フラット化して各要素を2乗

[ j for j in i for i in arr ] と書きたいところ。なぜ arr と要素変数 i, j がこのような並びになるのか。


Python は列挙を扱える点において古い言語よりアドバンテージがあるが、その文法は

  • 列挙走査 (for) は後置する (ただし、複数の for はネストの上位から下位に向かって並べる)
  • if は後置する (if - else 三項演算子は後置 if の語順を踏襲した発展形)

というようになっている。

Perl の if 修飾子を参考にしたのか、全体的に SQL の語順に似ているが ... と想像をめぐらすが、いずれにせよちぐはぐ感は否めない。列挙に対する map, filter, reduce 操作をスムーズに記述するだけの統合感・先進性には欠けており、思考を乱す順序でしかロジックを記述できない言語であると思う。データサイエンティストに好まれるというのが信じられない。

Training in Commercial Pilotage with FTD

都内某所で Flight Simulator による F/O 訓練。B737-800 で 2.0H。

Straight & Level Flight

いつものとおり Take Off from RJTT RWY 34R から 6,000 ft, 240 kts での Straight & Level Flight。

ADI での Pitch 維持を中心に据えることを心掛け、前回よりも安定するようになってきたが、まだ 2 分で安定させるに至らず。
今回は特に Tips なし。ただイメトレあるのみ。

Training in Commercial Pilotage with FTD

都内某所で Flight Simulator による F/O 訓練。B737-800 で 2.0H。

Straight & Level Flight

いつものとおり Take Off from RJTT RWY 34R から 6,000 ft, 240 kts での Straight & Level Flight。

前回つかんだコツを活かし VSI のトレンドを見てフゴイドを抑制するも Stabilize せず、想定していたよりもあきらかに調子が悪い。今日は Trim Wheel が回らず Pitch 感覚が狂ったのも理由の一つではあるが、それが主要因だとは思えない。集中してようやく Allowance ± 1 kt, ± 20 ft, ± 50 ft/min の幅に収まる程度、少し集中が途切れるとその倍くらいに簡単に振れてしまう。いつもは褒められる Bank も 1° ズレで Heading が最大 4 ° 揺らぐと元 ANA の K 教官に指摘される。夏の暑さで消耗し、集中力をやや欠いているものの、それでもなお Allowance に収まる程度の技量になってなければならない。

Allowance を逸脱するのは、Over Control が原因であることは自覚しているが、それを抑える方法がわからない。そこで Yoke をホールドしたまま N 教官のデモ操縦をトレースし、その後、教官とディスカッションする。指摘されたのは、私の操縦では、操作した後、機体が反応する前に早々に「反応しない」と判断してもっと踏み込む追加操作をしている、ということ。自覚あり。実際にすべきは「じわじわとゆっくり操作」し、その後、機体が所望方向に動き出すまで「待つ」、そして所望方向に動き出したら追加操作をむしろ「半分戻す」方向に行うということ。そして半分戻し後に安定維持させる Pitch 水準をあらかじめ見積もっておく。4 つの気づきあり。

  1. Pitch 操作でも当て舵のような「半分戻し」操作が必要
    • 目からうろこ。いまさらながらの気づきだが、言われてみれば当たり前。Aileron を傾け Bank するのと同様 Elevator を傾け気流を曲げて Pitch を動かしているのであり、当て舵・半分戻ししないと、静止状態から動かし始めたのと同程度の強い力を受け続け、曲げた方向にどんどん傾いてしまう。
  2. VSI のみで調整せず、操作目標を Pitch 水準におく
    • VSI は参照するものの、操作の目標はあくまで Pitch 水準を安定させることであり、その結果として VSI = 0 となる。
  3. 安定維持させる水準をあらかじめ見積もっておく
    • Stabilize するであろう Pitch そして Power をフゴイドで行きつ戻りつする間にも見定めておく。
  4. 脇を閉めたり、肘掛けを使って、ニュートラル・ポジションを感知できるようする
    • いままで脇が甘い中でよくぞこれだけ繊細なコントロールができたと逆に褒められた。

VSI は見なくても操縦できると、最後に VSI を隠して Partial Panel Instrument Flight する。すると Yoke 操作が慎重になり、いい塩梅に。これまでいかに VSI に振り回され過剰に反応しすぎていたことか。

本日の重要ポイント

  • 操作は当て舵・半分戻しする
  • 操作目標は VSI のトレンドではなく Pitch 水準とする
  • 安定維持する Pitch, Power 水準をあらかじめ見積もる
  • 脇を閉めてニュートラル・ポジションを作る

Functional Programming w/ C# LINQ

業務ユーザが既存 DB を読み取り専用で利用するための LINQ to Entities (Code First) コーディング・メモ

半月の間かなり苦労して Entity Framework 6 (EF6) と Entity Framework Core (EF Core) にてデータベースアクセスできるようになった。

業務ユーザとなり、開発者だった過去を忘れていたが、そもそもデータベース・アプリ開発は難しい、ということを思い出した。低級の疎通から始めて高級な要求を満たすまでに超えるべき壁がたくさんある。LINQ to Entities は QueryProvider という Runtime Compiler が C# ロジックから変換した SQL を発行するため、さらに壁が増え、難しくなる。加えて、LINQ to Entities は誕生から 10 年ほどたったものの、いまもまだ進化しており、バイブル的な参考書籍・情報がない。ネットには LINQ to SQLLINQ to Entities EntityFramework 4 / 5 / 6 / Core と進化してきた過程の情報が混在しており、初学者にはそれらの区別がつきにくくきわめてハードルが高い。また、当然ではあるが開発者観点での情報が多く、(少し技術寄りの) データ利用者が参考にするには情報があまりに断片的で、課題がなかなか解決につながらない。

そこで、業務ユーザが既存 DB を読み取り専用で利用する目的で LINQ to Entities を導入する場合にスムーズに実現できるポイントを備忘録としてここに記す。
Entity Framework 以外の動作検証環境*1Oracle XXX, Windows 7, Visual Studio 2017, C# 7.3, ターゲットは .NET Framework 4.7.1。

計画編

  1. プラットフォームの選択
    EF6 と EF Core のどちらを利用するか選択する。その前に両者ができること、できないことをよく把握する。EF6 よりも EF Core の方が全般的に簡易な設定で動作するため、既存 DB を読み取り専用で利用するには便利かもしれない。しかし、EF Core は、将来、破壊的変更がありうる実験的な実装であり、LINQ メソッドを SQL へ変換する QueryProvider の内部実装が EF6 とはかなり異なるため、ロジックに互換性がない。EF6 より EF Core の方が EntityFunctions (for EF5) / DbFunctions (for EF6) 等を使わずとも C# 記法のロジックをそのまま SQL 変換してくれる幅が広く *2、使いやすい面もあるが、一方で残念ながら現時点では GroupBy() メソッドに対応していない等の制約もある。両者の Pros / Cons を慎重に検討し、プラットフォームを選択する必要がある。
  2. Code First の採用
    LINQ to Entities の開発アプローチは Database First, Model First, Code First と 3 種類ある。既存 DB を利用するのだから Database First だろうと思いきや、前 2 者は古いアプローチであり、オススメは開発の手間が少ない Code First だ。ただし、Code First は Migration の項目で後述するようにコードで DB を作り変えることができる仕組みであり、すでに運用している DB に適用するには若干のリスクがあるため注意が必要。それがこのメモを書き留めておく理由でもある。
  3. Visual Studio ソリューションプロバイダーの「追加 > 新しい項目 > ADO.NET Entity Data Model の追加」手順は不要
    LINQ to Entities で開発する際には「Visual Studio で "ADO.NET Entity Data Model に追加" メニューを選択しろ」とネット上の諸解説では手順説明されており、このメニューで開発アプローチを選択するのだが、既定では SQL Server を対象とした設定しかなく、接続先 DB ごとのドライバー等をインストールしなければ手順が先に進まない。結論から言えば Code First を採用する場合、このメニュー選択とドライバー・インストールは不要 *3 だ。この手順を踏むことでなされるのは、DbContext を利用したテンプレート的なクラスの .cs ファイル作成と app.config / web.config の編集だけのようである。後述のコーディング手順を踏めばこの作業の内容はカバーされる。
  4. NuGet パッケージの導入
    Visual Studio の NuGet Package Manager で必要なライブラリを導入する。

    • EF6 + Oracle の場合
      • EntityFramework
      • Oracle.ManagedDatabase.EntityFramework
      • Microsoft.Extensions.Logging (ログ出力をする場合)
    • EF Core + Oracle の場合
      • Microsoft.EntityFrameworkCore.Design
      • Oracle.EntityFrameworkCore
      • Microsoft.Extensions.DependencyInjection (ログ出力をする場合)
      • Microsoft.Extensions.Logging.Debug (ログ出力をする場合)

  5. 接続先 DB の各種情報の調査
    当たり前のことであるが、データソース名 (alias ではなく、tnsping コマンドで取得できる文字列)、テーブル名、ユーザ名、パスワード、スキーマ名を事前に調べておく。特に EF6 ではスキーマ名の指定が必須になるため、正確な情報*4を入手しておく。

コーディング準備編

このブロックの手順をひととおり踏むまではテストランすらもしてはならない。ここを疎かにして走らせると、意図せぬ Create Table 文を発行してしまって焦ったり、何が悪くて問題が生じるのかわからず混乱するハメに陥る。

  1. 接続文字列を確認する枠組みの整備
    これまた当たり前のことではあるが、実際に接続に用いられた接続文字列を接続後に出力して確認する枠組みを整備しておく。LINQ to Entities では接続文字列を new DbConnection() 等でコードから与える方法と app.config / web.config 設定ファイルから与える方法が併存しており、プログラマが意図しない接続をしている可能性があるため、与えた (と思っている) 接続前の情報を信じてはならない。また、Code First ではコードが示す DB の有無や構造と実際の DB の状況が異なる場合に設定次第でプログラムが DB やテーブルを新規作成・変更しようとする可能性があるため、実際の接続先が何であるかを確実に確認することが混乱しないための初手となる。
  2. ログ出力の整備
    実際にどのような SQL が発行されているか確認できなければ、LINQ to Entities の挙動を制御する設定はできないため、ログ出力の仕組みを整備しておく。ログ出力の設定は EF Core よりも EF6 の方が簡単 (コードの記述方法は後述)。ログはアクションのたびに生成されるため、標準出力や標準エラー出力の場合は即時出力でよいが、ファイル出力の場合はログを StringBuilder にでも溜めておいてから DbContext 破棄時に FileStream で書き出すという工夫が必要。
  3. 直接発行 SQL と EntityFramework 作成 SQL を比較する枠組みの整備
    EntityFramework の設定と挙動の関係は慣れるまで把握しづらいため、同一の DB 操作について DbConnection.CreateCommand で直接発行する SQL と EntityFramework が作成し発行する SQL とを比較する枠組みを整備しておく。これにより、想定通り EntityFramework が動作しているか、想定通りに DB 接続しているか、想定通り DB が動作しているか、を切り分ける。当然、同じ接続文字列オブジェクトを使用する等、環境条件を同一に揃えて検証する。
  4. Migration は不要
    ネットで Code First を調べると Migration 手順が合わせて記事になっているが、既存 DB を読み取り専用で利用する場合には Migration は不要である。初学者にはわかりにくいが、Migration とはコード (POCO エンティティ・クラス) で記述される DB 構造と実際の DB 構造を同期させる Code First 特有の仕組みのことだ。そのため、既存 DB を読み取り専用で使う場合は Migration をしない、というより、してはならない。わかりにくいのはネット上の諸解説が「Code First は記述したコードから DB を作成することも、"既存 DB" からコードを興すことも可能」と説明していることだ。記事をよく注意深く読むと、後者の "既存 DB" とは、今回対象にしているようなすでに運用していて構造が固定化されている DB のことではなく、これから開発するために種として作成した開発初期モデルとしての DB のことを想定していると思われる。したがって、この "既存 DB" を今後 Migration してガシガシと構造変更する想定で解説されていることから、これを鵜呑みにして今回の目的のために同じ手順を踏んではならない。一番簡単なのは、むしろ、前者のように実際の DB と同じ構造の POCO エンティティ・クラスを予め作成しておき、Migration と自動追跡を無効にして接続することだ。コードと DB の同期をさせないためには、下記のように DbContext の自作派生クラス (ここでは MyDbContext とする) の static 初期化ブロックで Database.SetInitializer(null) とする*5。ここでのポイントは Database クラスの前に名前空間を指定すること。なぜなら、基底クラス DbContext にも Database という名前のプロパティがあり、コンパイラがそちらと混同し、static ブロック内ではインスタンス・メンバーへアクセスできないというエラーが発生してしまうためだ。

    ///【EF6 の場合の記述】
    ///  EF Core では他で行った読み取り専用の設定が効いたためか、この設定がなくても DB 構造更新の SQL 文が発行されることはなかった
    ///  ように思われる。System.Data.Entity は EF6 用の名前空間であるため、EF Core にそのまま適用することはできない。
    public MyDbContext : DbContext {
      static MyDbContext() {
        System.Data.Entity.Database.SetInitializer<MyDbContext>(null);
      }
    }
    


コーディング編 (EF6 版)

  1. MyDbConfiguration の記述 (EF6 のみ)
    app.config / web.config の修正
    接続文字列と同じく、
  2. MyDbContext コンストラクタの記述
  3. OnModelCreating() メソッドの記述
  4. SaveChanges() メソッドの記述

  5. DbContext.Set() メソッドの利用
    Visual Studio が作成するテンプレートでもネット上の情報でも、DbContext 派生クラス内に参照テーブルを下記のように記述せよと解説する。

    public MyDbContext : DbContext {
      public DbSet<TEntity1> TEntity1 { get; set; }
      public DbSet<TEntity2> TEntity2 { get; set; }
      // ... 以下略 ...
    }
    

    これはこれで接続・動作試験のために一度はやってみるべきだが、読み取り専用として使用するのに public set 可能であることとテーブルすべてをプロパティとして記述することが無粋で気に食わない。かといって public 以外のアクセスレベルにしたり set を落とすとコンパイルエラーや不具合になるようだ。Entity Framework が何らかのアクセスをしているからだろうとはいえ、ユーザ定義クラスのプロパティ宣言をしているだけであるため、その実体は他にあるはず。調べると DbContext 基底クラスの Set<TEntity>() メソッドや Set(Type) メソッドがそれらしい。これらの返り値が DbSet<TEntity> 型であり、それにクエリを加えると DbQuery<TEntity> 型になるようだ。いずれも IQueryable<TEntity> の実装。そこで MyDbContext クラスではプロパティ宣言する代わりにジェネリックで読み取り専用メソッドを作成し、一方で DbSet メソッド 2 つを直接操作できないように override する。

    public MyDbContext : DbContext, IDataSourceContext {
      public IQueryable<T> ReadTable<T>() where T : class, new() =>
        base.Set<T>().AsNoTracking(); // .AsNoTracking() でデータ変更の追跡を行わない = テーブル個別に ReadOnly とする
    
      public sealed override DbSet Set(Type entityType) =>
        new NotSupportedException($"{this.GetType().Name} は読み取り専用コンテクストであるため、 Set() メソッドの直接呼出しはできません。");
    
      public sealed override DbSet Set<TEntity>() =>
        new NotSupportedException($"{this.GetType().Name} は読み取り専用コンテクストであるため、 Set<TEntity>() メソッドの直接呼出しはできません。");
    }
    

    ついでに、この IQueryable<T> ReadTable<T>() where T : class, new() メソッドを含むインターフェース (ここでは IDataSourceContext とする) を定義し、MyDbContext も LINQtoCSV を通じて読み込むデータを管理する CSV のコンテクストもこのインターフェースを装備すれば、データ源泉に RDB / CSV の違いがあろうとも無差別・透過的にデータ処理することができる*6


  6. POCO エンティティ・クラスの記述

  7. POCO エンティティクラスの記述は下記のサンプルのとおり。

    public class SampleEntity : Entity {
      public DateTime Column0 { get; set; }
      public string        Column1{ get; set; }
      public int?           Column2 { get; set; }
    
      public void SetAttribute(DbModelBuilder modelBuilder, string schema) {
        var entity =
          modelBuilder.Entity<SampleEntity>()
            .HasKey( poco => poco.Column0 );
    
        if (schema != null)
          entity.ToTable(this.GetType().Name, schema);
        else
          entity.ToTable(this.GetType().Name);
      }
    
    public abstract class Entity {
      public abstract void SetAttribute(ModelBuilder modelBuilder, string schema);
    }
    

    フレームワークに依存せずにデータコンテナとして最小限の記述しか要求しない POCO スタイル、データコンテナの属性をアノテーションではなく外部から定義できるようにする Fluent API は美しい。テーブル名はエンティティ固有の情報のため POCO クラスの情報を引用し Reflection で与えたい、一方でスキーマはエンティティ依存の情報ではないため、外から Fluent API で与えたいところ。ところが、EF6 のSystem.Data.Entity.DbModelBuilder の Entity() メソッドは Type を引数で受けるオーバーロードはなく、型引数を受け入れるジェネリック版しか用意されていないため非常に扱いづらい。また ToTable() メソッドには引数を 2 つ取るもの (第 2 引数はスキーム名) と 1 つ取るものがあるが、前者の第 2 引数に何を与えたら後者と同じになるのか Microsoft Docs に記述がないために条件分岐で使い分けせざるを得ず、上記のような汚い実装になった。

  8. クエリ文字列の置換ロジックの記述

コーディング編 (EF Core 版)

  1. MyDbConfiguration の記述
    EF Core の場合は記述しなくても想定通りに動作した。
  2. MyDbContext コンストラクタの記述
  3. OnModelCreating() メソッドの記述
  4. SaveChanges() メソッドの記述
    EF Core の場合は DbContext 全体1を追跡なしに設定できるため、fail safe 目的で override しなくても問題がない。
  5. DbContext.Set() メソッドの利用
    EF Core の場合は DbContext 全体を追跡なしに設定できるため、読み取り専用のラッパーをかける必要がない。
  6. POCO エンティティ・クラスの記述
    EF6 の場合とまったく同じ。


ポイントは数多くあるが、総じていえる思想は「いきなり完成形を目指さず、実績のある最も簡単なサンプルから始めて一歩一歩進める」ということだ。データベース・アプリ開発で発生する問題は複合的であり、要因は 1 つとは限らないため、うまくいく事例と比較して問題を 1 つ 1 つ切り分けて進める必要がある。

*1:部分的に Windows 10, Visual Studio 2019, C# 8, .NET Core 2.2 でも実験してみたが、趣旨に影響はない。

*2:日付処理について、EF6 では DbFinctions の固定値しか受け入れられない日付関数を使うしかないが、EF Core では new DateTime() も .AddDays() もそのまま使える。しかも EF Core の .AddDays() は変数を受け入れられるため応用が利く。

*3:ドライバー・インストールは EDM を作成する Database First, Model First のときにのみ必要なのだろう。

*4:Oracle では一般にユーザ名と同じスキーマが使われるが、ユーザ共通のスキーマを通じてアクセスさせる DB もあり、この辺りを誤ると不毛なトラブルシューティングに労力を費やす。

*5:Database.SetInitializer() の記述により同期の方法を、既存 DB がなければ Create Table、常に Drop Table & Create Table、等に切り替えることができる。開発フェーズの進展によって切り替えられるのは便利だが、引数 1 つで Drop Table しうるのはかなりリスキーである。

*6:業務ユーザとしては、純粋に RDB のみ扱うわけではなく、CSV とハイブリッドでデータを捏ねることがある。ちなみに new() 制約は、空のデータコンテナに文字列を Parse してデータを埋め込む CSV 側から来るもの。

Functional Programming w/ C# LINQ

ここ 9 ヶ月ほど C# LINQ プログラミングをしてきて、関数型・集合論的演算はやはり素晴らしいと思ってきたが、コーディングの目的が調査・研究から運用目的のシステム構築に移りつつあるためか、LINQ の限界が見えてきた。

C# は unsafe で書けば C / C++ 級の低水準コーディングが、managed で書けば Java 級の高水準コーディングが、LINQ で書けば超高水準な関数型・集合論的コーディングが可能になり、可読性と開発効率が飛躍的に向上する。水準を選べるその幅広さと、OnMemory・RDB へのデータアクセスが透過的・無差別に記述できる LINQ という他の言語にない OR マッピングアプローチがすばらしいのだが……… unsafe → managed → LINQ と高水準になる過程で抽象化が図られているわけであり、抽象化とは実装を切り離すこと、実装はコンパイラあるいはランタイムが担い、プログラマが管理しなくてよい (≒できない) とすることであることを再認識した。

このことに思いを致した結果、「実装を切り離す」ということの弊害が目につくようになってしまった。

LINQ を多用してきたものの .GetEnumerator() は2、3度ほどコピペでしか書いたことがないため見過ごしてきたが、想定通りに動かないケースに対応しようと先週、ある .GetEnumerator() を独自に改良した。そして、LINQ を使い始めて 10 年になるが、ここにきて初めて IEnumerable<T> が内部でどのように動作しているのか理解した。

IEnumerable<T> は OnMemory でも RDB でも同じ記法で透過的に記述できる魔法だが、その名のとおり、シーケンシャルアクセスしか定義せず、ランダムアクセスを用意しない。n 番目の要素を取得するには .ElementAt(n) とするが、これは配列のインデクサアクセス array[n] とは異なり、.Skip(n).First() と同じ *1であり、実際に LINQ は .ElementAt(n) が呼ばれるたびに、.GetEnumerator() して列挙子を取得し、先頭から n 要素スキップして最初の要素を返す、という気の遠くなるような手順を踏む。

これをインデクサと同じくランダムアクセスとするようショートカットする (か否かを決める) のは、IEnumerable<T> インターフェースを持つデータ構造 *2 の実装の方であり、IEnumerable<T> インターフェース自体は我関せずという態度なのだ。また、いまのところ、C# コンパイラ .NET ランタイムも IEnumerable<T> やその実装の内部の挙動を最適化しない。

OnMemory データ操作も RDB データ操作も記述を可能な限り統一しようとしてきたが、実装の違いがある以上、実行効率とのトレードオフを勘案すると限界がある。LINQ は可読性を高めるが、やりすぎると却って可読性が落ちることも経験上わかっている。

LINQ の使いどころはやや控えめに考えることにし、C#LINQ 最適化チューニングするまでは脱 LINQ 至上原理主義を宣言しようと思う。

*1:LINQ to Entities で RDB アクセスするにはこの記法にする必要がある。

*2:固定長構造の配列やリストならランダムアクセスが可能だが、FileStream などの可変長構造ならランダムアクセスは不可能である。