計画駆動とSHIROBAKOの進行係の関係

SHIROBAKO http://shirobako-anime.com/ は、アニメ制作会社のアニメな訳ですが、主人公の宮森あおいはアニメーションを描けません。テレビアニメーションを作るのに、絵を描くアニメーターの存在は不可避なわけですが(キャラデザとか作監とか監督とか企画とかも)、このなかで制作会社として「進行」係に焦点を当てたアニメ作品です。

以前、ブログで書いた後に http://www.moonmile.net/blog/archives/10049 に続きを書こうと思ったら、京都アニメーションに放火事件があって、その後を続ける気分にならなかったのですが、ちょっと思ったことを書き下しておきます。

by SHIROBAKO

アニメーション制作と計画駆動の類似点

SHIROBAKO はアニメ制作会社の話なので、ある意味でソフトウェア開発の計画駆動(ウォーターフォール開発)に近いところがあります。いわば、

  • 最終締め切り(納品)が明確に決められていて、動かせない
  • プロジェクト進行の手順がほぼ決定されていて、前後しない
  • 前のタスクが遅れると後ろのタスクに影響がでる

というところです。一般的なアジャイル開発では、予算の変更やリリーススケジュールの変更が可能であるのですが(ここができないと、あまり「アジャイル開発」とは言えません)、従来型の計画駆動の場合はリリース日が決まっていたり、途中のマイルストーンが決まっていたりします。例えば、アプリのリリース日がプレスリリースされている場合は、その納品日を動かすことはまずできません。システム運用の切り替えや、引継ぎなどの関係、法律の関係など、納品日が絶対であるソフトウェア開発は結構あります。
この場合、余裕のある納品日と予算とスケジュールを立てればよいのですが、ソフトウェア開発において不可避な状態に陥ると、この「納品日」を守るが、かなり大変になりいわばデスマーチ化します。

ソフトウェア開発において、設計→製造(コーディング)→試験の順番は、XPやスクラムなどのアジャイル開発、イテレーション開発などによって一定ではないことが多くなってきました。しかし、多数の会社が参加していたり、ハードウェアとソフトウェアの両輪が回っていたりすると、設計→製造の順番が変えられないことが多くあります。この場合、従来型の計画駆動を踏襲して、概要設計や外部設計を重視したりします。

SHIROBAKOのアニメーション制作においても監督のシナリオ作りからラフ絵、作監、動画、撮影などの順番は変わりません。さすがにこれを前後することはできません。この順番をうまく回していくために、アニメ制作会社ではマネジメント業務として「進行」係が割り当てられます。昔のアニメーションを見ると「進行」の担当者が必ずいます。

進行係が何をするかと言うと、上司的な「マネージャー」役をやるわけではありません。SHIROBAKOを見るとわかるのですが、主に雑用係っぽいところが多いです。しかし、制作会社の立場から監督、作監、動画などのフリーランスあるいは外注を含めて、とりまとめをしていく重要な役割です。ある意味で、各フリーランスや外注の会社がきちんと〆切を守るのであれば、進行係の仕事は必要なく、非常に暇ですよね。でも、そんなことはありえません。確率的にゼロに等しいのです。なので、不確定要素を含みつつ、うまくアニメーションが出来る上がるまで(放映日まで)なんとかするのが「進行係」の重要な仕事です。

ソフトウェア開発においても、マネージメントという業務があり、権限の強いマネージャーという組織化をすることもあるのですが、果たしてそれがうまく廻るかどうかはわかりません。従来のウォーターフォール開発からアジャイル開発に移行しつつあるように、単なる工場的な上司部下的な分け方でソフトウェア開発ができなくなっています。そういう場所でマネージャーの行う「マネジメント」とい業務は何をするべきなのか?という疑問がある常々おこるのですが、これの答えのひとつが「進行係」です。

進行係はプレイングマネージャーではない

SHIROBAKO の主人公宮森あおいは、絵が描けません。高校時代のアニメ部?の仲間はアニメーターや声優になるわけですが、宮森だけは直接アニメーションを作る作業ができるという訳ではないのです。しかし、本作品で重要なのは、実際にアニメーションを作る(ソフトウェア開発ではコーディングをする、設計をするなど)訳ではなく、全くそれ以外の場所でもアニメ制作には重要である、という処に焦点があります。

ソフトウェア開発における「プレイングマネージャー」という役割は、アジャイル開発スクラムのリーダーやスクラムマスターっぽい位置に属します。開発グループにおいて、ある程度コードが書ける=尊敬に値するというカリスマ性は一種重要なものを含んでいるのですが、これがプロジェクト成功において必須条件という訳ではありません。プロジェクト成功の基準を

  • スケジュール通りに納品すること
  • 予算内で終わらせること
  • プロジェクトメンバが脱落しないこと(退職など)

のように、ヒト・モノ・カネの軸で言えば、プロジェクトマネージャのカリスマ性はあまり必要ではないことがわかります。たまに、三番目を外して「プロジェクトを成功」させるマネージャが多いのですが、会社としては継続し得ないプロジェクトメンバ=社員を作ってしまうことは大きな損失なので、プロジェクトが成功したとは言えないでしょう。

ゆえに、ソフトウェア開発におけるマネジメント業務を「進行」という雑用っぽいところまでに落とし込んでいくと、プレイング=コード書ける、必要がないことがわかります。また、ドラッガーがいう処のマネジメント=あれこれ調節する役割も必要ありません。

進行係においては、進行係自身には権限がないのですが、それぞれの権限のある人(監督や作監、動画など)のパフォーマンスが十分に保たれるように動きます。ソフトウェア開発で言えば、プログラマ、営業、テスター、システムエンジニアのように別々の特殊技能を持っている人のパフォーマンスをあげるように調節する役柄です。これを「マネジメント」と言うのですが、一般的な「マネージャー」と区別するために「進行係」という名称を流用します。

進行係は何に注力するのか?

計画駆動における進行係の存在は、現在のところ(といはいえ既に思いついてから20年以上たっているけど)目立った形で見えるわけではありません。本来ならば、マネジメント業務のひとつとして「進行」の役割を演じればいいのですが、

  • 顧客交渉の強い立場としてのマネジメント
  • 進行を円滑するするための下支えのマネジメント

とが、なかなか共有できるとは思えません。とは言え、私の個人的な経験では2名ほど上記の2つが同時実行できるマネージャを知ってはいるのですが、どうも個人的な素質の部分が大きく、体系化できそうにないです。

なので、計画駆動においては、強い立場のマネジメントを従来型のマネージャーが担い、プロジェクト進行を滞りなく進めるマネジメントを進行係として別に割りあてるのがよいでしょう。
とは、いえ、SHIROBAKO のような大所帯(アニメーション制作には100人以上に人がかかわっています)とは違い、昨今のソフトウェア開発のような小規模(5人程度)のプロジェクトにおいて「進行係」を別に立てることができるかといえば、そうはならないでしょう。30人規模のプロジェクトならば、進行係がいたほうがいいかなと思うのですが、現実問題として、独立したひとに「進行」を任せるのは小規模の開発プロジェクトでは無理です。なので、アジャイル開発のように混在した形になりがちです。

締め切り厳守するための進行の役割

となると、ソフトウェア開発において「進行」の役割は、単一の人に割り当てるのは無理そうです。適用範囲が狭すぎるので、あまり現実的ではありません。

ただし、先に書いたようにアジャイル開発が締め切りを動かすことを前提としているように(場合によっては、スクラム開発で強引に締め切りを守る方法もありますが、そこは「機能」を調節しますよね)、計画駆動のように締め切りを前提とした開発スタイルにもメリットは大きいのです。

アジャイル開発において、顧客の要求の変動により仕事量が上下することを前提としていますが、ソフトウェア開発においてはある程度までこの変動を抑えられることがあります。いわば、商品開発的な受託開発や提案型の受託開発です。この場合は、アジャイル開発が使われることが多いのですが(それを売りにする場合もあるし)、敢えて変動要素を少なくするようにすれば、進行的な役割を据えて無理なく締め切りを迎えることが可能になるでしょう。

そこは予測、と変動を許容するプロジェクトバッファという形になるので、これはアジャイル開発自身にも使われているところです。

  • バーンダウンチャートとアーンドバリューの組みあわせ
  • 工事進行基準の応用
  • チケット駆動におけるトータルチケットの予測値

古くは CMMI のレベル4と5の中間あたりですね。CMMI 自体が消え去ってしまっていますが、参考にできるところは多いです。あと、稲森会長の「アメーバ経営」を参考に。

参考

アニメ制作について|TVアニメ「SHIROBAKO」公式サイト http://shirobako-anime.com/about.html

カテゴリー: 開発 | 計画駆動とSHIROBAKOの進行係の関係 はコメントを受け付けていません

.NET MAUI から Firebase を利用する

.NET MAUI が Visual Studio 2022 の正式版にアップデートされたので、適度にツールを作ってみます。のテストです。ちょっと、手元で入用な Firebase 接続確認のアプリを作る必要があってので、.NET MAUI を使ってアプリを作っていきます。

と、結構すんなりいく筈だったのですが、意外と難関だったので、備忘録として残しておきます。

Visual Studio 2022 で .NET MAUI プロジェクトを作る

ツールの対象として iPhone アプリになるので、iOS のみを対象としています。

.NET MAUI は Xamarin.Forms とは異なり、

  • コードが .NET 6 ベースになる(内部的には mono だけど、NuGet のライブラリが .net 6 で揃えられる)
  • 各プラットフォームは #if で切り替える
  • 各プラットフォームはひとつのプロジェクトになる

となります。

ちょっと、ややこしいのは各プラットフォーム(iOS, Android、MacCtalyst、UWP)のコードがひとまとまりになって、ビルドをするたびに4つのプラットフォームを全部ビルドします。なので、

  • ビルドに少々時間が掛かる
  • 別プラットフォームでビルドできないコードを含めてしまうと、エラーがうっとおしい。

という状態に陥ります。

たとえば、モバイルアプリの場合は、iOS用とAndroid用しかいらないわけで Windows(UWP)のほうビルドエラーがでると面倒なことになる、のですが、どうするのかは微妙なところです。多分、*.csproj で TargetFrameworks を絞ればいいと思うのですが。

ひとまず、ビルドして実機で動かす

実は、「逆引き大全2022」と「.NET6本」の .NET MAUI の章は Visual Studio 2022 のプレビュー版を使っています。概ね今回の正式版と変わらないのですが、プレビュー版ではiOS シミュレータの動作が不安定であったり、iPhone 実機で動作できなかったりしました。

なので、さっそく実機で動かしておきます。

画面のほうは、実機ではなくシミュレータのものですが、Visual Studio 2022 からデバッグモードで問題なく動いています。

iOS 版を作るときは、Mac とのペアリング等もろもろの設定が必要のですが、これは Xamarin.Forms のときと同じです。

.NET MAUI と Xamarin.iOS が共存できない?

.NET MAUI プロジェクトを作るときの注意点ですが、Mac に .net 6 ベースの mono(かな?)を入れてしまうらしく、従来の Xamarin.iOS との共存ができません。正確には、

  • Windows 上の Visual Studio から .NET MAUI のプロジェクトを作成し、Mac に接続する
  • Windows 上の Visual Studio から Xamarin.iOS のプロジェクトを作成、Mac に接続する

ことができません。mac 上の mono のバージョンが異なるようで、Xamarin.iOS pうろジェクトが動きません。

ただし、

  • Windows 上の Visual Studio から .NET MAUI のプロジェクトを作成し、Mac に接続する
  • Mac 上の Visual Studio から Xamarin.iOS のプロジェクトを作成し、Mac 上で動かす。

ことは可能です。プロジェクトに設定されている mono のバージョン違いなのかもしれません。

Firebase に接続するための NuGet をインストール

Xamarin.Forms のときにどうだったか忘れてしまったのですが、Xamarin から Firebase に接続するための NuGet パッケージは、Android と iOS では別々のものが提供されています。コマンド自体は HTTP 接続だろうから、共通なのでは?と思ったけど暫くハマりました。

iOS 版の NuGet パッケージは以下になります。

https://github.com/xamarin/GoogleApisForiOSComponents

  • Xamarin.Firebase.iOS.Core
  • Xamarin.Firebase.iOS.CloudFirestore
  • Xamarin.Firebase.iOS.Auth

の3つ入れておけば大丈夫です。

ちなみに Android 版のほうは「Xamarin.Firebase.Common」のように、「iOS」がないものを使うので注意が必要です。これ、なんらかの形でまとめてくれませんかね。Windows/UWPの場合は不明です。。。

サンプルコードは

https://github.com/xamarin/GoogleApisForiOSComponents/tree/main/samples/Firebase/CloudFirestore/CloudFirestoreSample

にあるので、参考にしてください。

Xamarin.Firebase.iOS.CloudFirestore パッケージを入れた後でビルドに失敗する

Xamarin.Firebase.iOS.CloudFirestore パッケージを NuGet で入れると、.nuget フォルダーにパッケージがダウンロードされます。が、これがビルド時に失敗してしまいます。

パッケージには *.h ファイルが含まれてい、なんらかの形で参照をしているのですが、Windows の PATH の制限があって、これがエラーになってしまうのです。

ひとつの解決先は

https://github.com/xamarin/GoogleApisForiOSComponents/issues/555

にある通り、環境変数 NUGET_PACKAGES を設定して「c:\Nugets」のように PATH が短くなるように工夫します。

ですが、これは根本的な解決策にはなりません。おそらく Visual Studio 2022 がビルドに使っているターミナルが cmd ベースなのが問題でしょう。試しに、Powershell を立ち上げて、独自に「dotnet build」とすると無事に長い PATH でエラーになる問題は解決されます。

Xamarin.Firebase.iOS.* パッケージが設定しようといている *.h ファイルは最初の1回だけで、あとはキャッシュが使われるようなので、NuGet パッケージの設置した後に一回だけ「dotnet build」あるいは「dotnet restore」しておくとよいです。

ちなみ、Visual Studio 2022 上でビルドエラーになる現象は、何度か発生します(不定期です)。この場合も、Powershell 上で dotnet build すると直ります。

Firebase に登録するコードを書く

  • Firebase.Core.App.Configure で初期化
  • Document を作成して、SetData で登録

すれば OK です。この部分は、Xamarin.iOS の頃と同じ筈なのですが、Xamarin.iOS と Firebase の組み合わせを使っている人が少なくて、探すのに苦労しました。Android のほうはそれなりにあるので、大丈夫だと思います。

private void MainPage_Loaded(object sender, EventArgs e)
{
#if IOS
		Firebase.Core.App.Configure();
#endif
}
/// <summary>
/// Firebaseに接続
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnClickConnect(object sender, EventArgs e)
{
#if IOS
		message.Text = "登録開始";
        var store = Firebase.CloudFirestore.Firestore.SharedInstance;
		var coll = store.GetCollection("contacts");
		var doc = coll.CreateDocument();
        var dic = new Dictionary<object, object>();
        dic.Add("device_id", "0000");
        dic.Add("name", "test");
		var time = Firebase.CloudFirestore.Timestamp.Create(
			(long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds, 0);
        dic.Add("update_at", time);
        doc.SetData(dic);
		message.Text = "登録しました " + DateTime.Now.ToString();
#endif
}

#if で囲ってあるのは、コードビハイドの MainPage.xaml.cs で記述しているので、iOS しかビルドが通らないためです。この部分は適宜 Android/iOS 共通のクラスを作っておいて、内部で #if で分けるのがベターです。Android と iOS で名前空間が違うので、Xamarin.Forms のときのようにインターフェースを駆使するよりも、#if でビルド時に切り分けてしまったほうが楽です。

時刻は、Firebase 側で Timestamp 型を扱うため、Firebase.CloudFirestore.Timestamp に変換して使います。

GoogleService-Info.plist を配置する

Firebase からダウンロードした「GoogleService-Info.plist」をプロジェクト内に配置させます。

これが苦労して2時間ぐらいかかりました。

GoogleService-Info.plist は、プロジェクトのルート(Plaftforms/iOS の下ではない!)において、手作業で、BundleResource を記述します。

	<ItemGroup>
		<BundleResource Include="GoogleService-Info.plist" Condition="Exists('GoogleService-Info.plist')" />
	</ItemGroup>

Visual Studio 2022 のプロパティでは、「BundleResource」を設定できません。日本語の「埋め込みリソース」は EmbeddedResource に変換されているので、多分 .NET MAUI の *.target あたりのバグじゃないかなと。

Android の場合は「GoogleService-Info.json」なので、拡張子が違ってダブらないのですが、Windows 版はどうだったかな、と。ファイル名がダブル場合は困ることになるので、今後問題になりそうです。

ただし、コードでファイル名を指定したりパラメーターを独自に設定したりする方法があります。

Cloud Firestore のルールを設定しておく

動作させた最初では、Firebase に登録ができなくて、.NET MAUI & Firebase の不具合では?と悩んでいたのですが、ルールがきつい設定になっていました。

ひとまず、動作確認をしたいときは、以下のようにルールを緩く設定しておくと便利です。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /contacts/{id} {
      allow read, write
    }
  }
}

実運用になると、request.auth でユーザー権限を調べて uid のチェックをしたほうがいいですよね。実際のアプリのほうはそうなっています。

実行する

アプリを実行するとこんな感じ。

Firebase のドキュメントに無事データが入っています。

カテゴリー: 開発 | .NET MAUI から Firebase を利用する はコメントを受け付けていません

.NET Framework 4.7 から .NET6 への移行ポイント

Windows 上の .NET Framework から .NET 6 へ移行するときに、障害となりそうなポイントを挙げておきます。ちょっとした Windows フォームや WPF アプリならばすんなり以降できるのですが、それなりの大きさの C/S 方式で Entity Framework を使っていると(使っているんだが)、移行時に躓きがありそうです。

移行対象

  • WPF アプリで約70画面ある。
  • テーブル数は約20テーブルある。
  • MVVMパターンを使い、View と ViewModel を分離させている
  • Model は .NET Framework の Entity Framework を使っている。コードファースト以前の、*.edmx ファイルを使う方式(SQL Server のテーブルからコードを自動生成するので当時は便利だった)
  • テーブル検索はほぼ LINQ を使っているが、移行前の複雑な SQL を使うため SqlQuery で直接呼び出している部分がる。
  • View にバインドさせるため、Entity クラス(*.edmx による自動生成)に、適宜プロパティをはやしている。
  • Excel のコントロールを埋め込むため、WindowsFormsHost を使っている
  • Excel の帳票をつかうため、Microsoft.Office.Interop.Excel を使っている

ステップ数は数えてはいませんが、大体1万行を超える位です。もともと、ACCESS 版を C# 版に書き直したもので、これを試しに .NET6 に移そうと試しています。

全体の構造

.NET Framework の WPF を .NET6 の WPF に移行するので、全体のフォルダー構造は変えません。 中のコード(特にロジック)には手をいれたくないので、namespace はそのまま使うようにします。

Entity Framework の移行

.NET Framework の System.Data.Entity.DbContext を .NET6 の Microsoft.EntityFrameworkCore. DbContext に移行します。同じ「 DbContext 」となっていますが、中身は別物です。.NET6 のほうは、NuGet で “Microsoft.EntityFrameworkCore.SqlServer” をインストールします。

.NET Framework の EF とは違い、*.edmx のようなテーブルの構造を定義したファイルがありません。コードファースト的にクラスを定義しておくか、データベースファースト的に SQL Server から dotnet ef コマンドを使いテーブルクラスを生成させます。テーブルクラス = Entity クラスは、以前の .NET Framework と同じものを使うので(プロパティ名などが変わるとやっかい).NET Framework 版のものをコピーして Models フォルダに入れておきます。

DbSet クラスは、

  • .NET Framework では System.Data.Entity.DbSet
  • .NET6 では Microsoft.EntityFrameworkCore.DbSet

となり同じ「DbSet」という名前になっていますが、似て非なるものです。

SQL Server に接続する文字列は、.NET6の場合(EFCoreの場合)は OnConfiguring を override して記述します。場合によっては、複数の DbContext を切り替える必要があるので、外部から切り替えができるようにしておきます。

public static 会議室Entities CreateEnt(string dbname, string servername = "(local)")
{
    var builder = new SqlConnectionStringBuilder();
    builder.DataSource = servername;
    builder.InitialCatalog = dbname;
    builder.IntegratedSecurity = true;
    var cnstr = builder.ConnectionString;
    var context = new 会議室Entities();
    context.Database.SetConnectionString(cnstr);
    return context;
}

主キーを付加する

.NET Framework の EF が出力する Entity クラスは主キー(Key属性)がありません。このため、この Entity クラス .NET6 の EFCore で使うと実行エラーが発生します。検索時ではあっても、デフォルトでは主キーが必要になっているためです。.NET Framework の場合はでは、*.edmx ファイルに主キーなどのテーブル構造の情報がはいっているため、Entity ファイルには Key属性等がついていません。

public partial class 会社基本情報
{
    public int id { get; set; }
    public string 会社名 { get; set; }
    public string 肩書き { get; set; }
    public string TEL { get; set; }
    public string FAX { get; set; }
    public string 郵便番号 { get; set; }
    public string 住所1 { get; set; }
    public string 住所2 { get; set; }
    public string 期名前 { get; set; }
    public Nullable<double> 消費税 { get; set; }
    public Nullable<int> 端数処理コード { get; set; }
    public Nullable<int> 年 { get; set; }
    public Nullable<System.DateTime> 期首日 { get; set; }
    public Nullable<System.DateTime> 期末日 { get; set; }
    public string 振込先A { get; set; }
    public string 振込先B { get; set; }
    public string FA { get; set; }
    public string FB { get; set; }
    public string GA { get; set; }
    public string GB { get; set; }
    public string HA { get; set; }
    public string HB { get; set; }
}

ここの id 部分に Key属性をつけてもよいのですが、このファイル自体はさわりたくないので、DbContext の OnModelCreating メソッドで調節します。

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<ホームページ>().HasKey(x => x.ID);
        modelBuilder.Entity<確認>().HasKey(x => x.日付);
        modelBuilder.Entity<期首>().HasKey(x => x.ID);
        modelBuilder.Entity<県>().HasKey(x => x.ID);
        modelBuilder.Entity<顧客>().HasKey(x => x.ID);
        modelBuilder.Entity<顧客SUB>().HasKey(x => x.ID);
        modelBuilder.Entity<予約>().HasKey(x => x.ID);
        modelBuilder.Entity<予約SUB>().HasKey(x => x.ID);
        modelBuilder.Entity<予約確認>().HasKey(x => x.ID);
        modelBuilder.Entity<予約状況>().HasKey(x => x.ID);
        modelBuilder.Entity<会社基本情報>().HasKey(x => x.id);

まめに HasKey で主キーを設定してやれば ok です。

生 SQL を呼び出したときの戻り値のクラスでは、主キーがないことを設定する

実テーブルを参照する場合には主キーがあるのですが、生 SQL を使って検索結果を返すときには主キーはありません。なので、.NET6 の EFCore では、HasNoKey を使って明示的に設定しないといけません。

全て LINQ で済めばよいのですが、複雑な join が発生する場合、生 SQL を使ったようが良い場合があります。例えば、以下のような SQL を LINQ に書き直すのはかなり困難ですし、移行時に間違いが発生します。なので、そのまま SQL を使います。

        var SQL = $@"
select
予約.ID,
顧客.会社名,
予約.日付,
部屋.名称 as 会議室,
予約状況.開始時刻 as 開始,
予約状況.終了時刻 as 終了,
顧客sub.部署名 as 部署名,
顧客sub.担当者A as 担当者,
顧客sub.TEL as TEL,
顧客sub.FAX as FAX,
予約.予約 as 状態,
予約.契約日 as 契約日,
予約.確認票最終処理日 as 確認日,
請求.日付 as 請求日,
X.利用料,
isnull(入金.入金額,0) as 入金額,
予約.備考B as 注意事項,
予約.案内名称 as 案内板名称
from 
( 
select
予約.ID as ID,
cast(ISNULL(sum(予約sub.単価*予約sub.数量),0) as money) as 利用料
from 予約
left join 予約sub on 予約sub.予約ID = 予約.id
where 予約.顧客id = {顧客ID}
group by 
予約.ID 
) X
inner join 予約 on 予約.id = X.ID
inner join 顧客 on 顧客.ID = 予約.顧客ID
inner join 顧客sub on 顧客sub.ID = 予約.顧客SUBID
left join 請求 on 請求.id = 予約.請NO
left join 入金 on 入金.請求ID = 請求.ID
inner join 部屋 on 部屋.id = 予約.会議室
inner join 予約状況 on 予約状況.予約ID = 予約.ID
order by 予約.日付 desc 
";
this.Items = App.ent.Database.SqlQuery<結果>(SQL).ToList();

この時、戻り値として使っている「結果」クラスは、.NET Framework の場合は適当に値クラスを作って検索結果を受け取ればいいのです(いちいち値をクラスを作るのが面倒ではあるのですが)。

しかし、.NET6 の EFCore には Database.SqlQuery に相当するものがなく、DbSet<T>.FromSqlRaw を使うことになっています。使い方は、以下のようにいったん DbContext に「結果」コレクションをはやすことになります。

            var SQL = $@"
select
...
";
this.Items = App.ent.結果.FromSqlRaw(SQL).ToList();

これに伴い、DbContext クラスには、戻り値のクラスも DbSet<T> で定義しないといけません。

        // 戻り値用
        public virtual DbSet<ViewModels.VM請求一覧.請求一覧> 請求一覧 { get; set; }
        public virtual DbSet<ViewModels.VM顧客履歴.結果> 結果 { get; set; }
        public virtual DbSet<ViewModels.VM予約一覧.Data> 予約一覧Data { get; set; }
        public virtual DbSet<ViewModels.VM予約詳細入力.引合> 予約詳細入力引合 { get; set; }
        public virtual DbSet<ViewModels.VM属性別集計.属性> 属性別集計属性 { get; set; }
        public virtual DbSet<ViewModels.VM年間グラフ.年間売上> 年間売上 { get; set; }
        public virtual DbSet<ViewModels.VM月別売上.月別売上> 月別売上 { get; set; }
        public virtual DbSet<ViewModels.VM請求集計.請求> 請求集計請求 { get; set; }
        public virtual DbSet<ViewModels.VM過去予約状況.結果> 過去予約状況結果 { get; set; }
        public virtual DbSet<ViewModels.VM顧客別利用状況集計.結果> 顧客別利用状況集計結果 { get; set; }
        public virtual DbSet<ViewModels.VM顧客利用回数一覧.利用回数> 顧客利用回数一覧利用回数 { get; set; }
        public virtual DbSet<ViewModels.VM顧客利用状況.利用明細> 顧客利用状況利用明細 { get; set; }

実は、戻り値クラスはそれぞれの ViewModel クラス内でしか使わないので private になっているのですが、.NET6 の場合には DbContext から参照できるように public(あるいはinternal)の変更が必要になります。はっきり言って、この FromSqlRaw の仕組みはよくないです。もともと内部クラスになるので、名前の重複が激しくなっています。

この戻り値専用のクラスは、主キーがありません。これを明示的に示すために OnModelCreating 内で HasNoKey を指定します。

            modelBuilder.Entity<ViewModels.VM請求一覧.請求一覧>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM顧客履歴.結果>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM予約一覧.Data>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM予約詳細入力.引合>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM属性別集計.属性>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM年間グラフ.年間売上>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM月別売上.月別売上>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM請求集計.請求>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM過去予約状況.結果>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM顧客別利用状況集計.結果>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM顧客利用回数一覧.利用回数>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM顧客利用状況.利用明細>().HasNoKey();

Entity クラスに NoKey 属性のようなものがあればよいのですが、どうも無いみたいですね。

データベース上のカラム名と一致させる

.NET6 の EFCore では、データベース上もカラム名と Entity クラスのカラム名が完全に一定していないといけません。よって、次のようなデータベース上では、正式な名前の場合、ちょっと困ることがおこります。

これは前回の移植上、予約テーブルに「予約」という名前のカラムがついています。

実は、Entity Framework では、テーブル名とカラム名が同じのは使えません。以前から問題だと思っているのですが、テーブル名もカラム名も同じ名前空間で混在させてしまっているからなのです。

なので、.NET Framework の EF では、以下のように「予約1」という名前が勝手に振られます。

    public partial class 予約
    {
        public int ID { get; set; }
        public int 顧客ID { get; set; }
        public int 顧客SUBID { get; set; }
        public Nullable<int> 予約者 { get; set; }
        public string 予約会社 { get; set; }
        public string 予約担当 { get; set; }
        public string 予約TEL { get; set; }
        public int 担当者ID { get; set; }
        public Nullable<System.DateTime> 日付 { get; set; }
        public Nullable<System.DateTime> 記録日 { get; set; }
        public Nullable<System.DateTime> 契約日 { get; set; }
        public Nullable<System.DateTime> M期限 { get; set; }
        public Nullable<System.DateTime> K期限 { get; set; }
        public Nullable<int> 予約1 { get; set; }
        public Nullable<int> 会議室 { get; set; }
        public Nullable<int> TF { get; set; }
        public Nullable<int> TE { get; set; }

ところが、.NET6 の EFCore では、データベースのカラム名と Entity クラスのプロパティ名は同一であるという前提があるので、以下のように Column 属性で変更しておきます。

    public partial class 予約
    {
        public int ID { get; set; }
        public int 顧客ID { get; set; }
        public int 顧客SUBID { get; set; }
        public Nullable<int> 予約者 { get; set; }
        public string 予約会社 { get; set; }
        public string 予約担当 { get; set; }
        public string 予約TEL { get; set; }
        public int 担当者ID { get; set; }
        public Nullable<System.DateTime> 日付 { get; set; }
        public Nullable<System.DateTime> 記録日 { get; set; }
        public Nullable<System.DateTime> 契約日 { get; set; }
        public Nullable<System.DateTime> M期限 { get; set; }
        public Nullable<System.DateTime> K期限 { get; set; }
        [Column("予約")]
        public Nullable<int> 予約1 { get; set; }
        public Nullable<int> 会議室 { get; set; }

この部分は、実は「予約」プロパティに変更してもよいのですが、既に各種のコードから「予約1」で参照しているので、参照先のコードは変更したくありません。よって、Column 属性でのがれておきます。

拡張したプロパティを DbSet に無視させる

.NET Framework の Entity クラスを MVVM パターンで View にバインドさせる場合、表示上フォーマットを変更させることがよくあります。Converter を作るのもよいのですが、どうせ partial になっているので、適当なプロパティを作って拡張させておくと便利、だったのです。

    public partial class 単発アラーム
    {
        public int ID { get; set; }
        public Nullable<System.DateTime> 日時 { get; set; }
        public string 対象種別 { get; set; }
        public string 対象名 { get; set; }
        public string 表示内容 { get; set; }
        public System.DateTime CreateAt { get; set; }
        public System.DateTime UpdateAt { get; set; }
    }

このような自動生成された Entity クラスとは別に、patial で拡張しておきます。

    /// <summary>
    /// 日時を年月日と時分に分ける拡張
    /// </summary>
    public partial class 単発アラーム
    {
        public Nullable<System.DateTime> 日時_年月日
        {
            get { return this.日時; }
            set
            {
                if ( this.日時.HasValue )
                {
                    DateTime dt = new DateTime(
                        value.Value.Year,
                        value.Value.Month,
                        value.Value.Day,
                        日時.Value.Hour,
                        日時.Value.Minute,
                        0);
                    this.日時 = dt;
                }
            }
        }
        public Nullable<System.DateTime> 日時_時分
        {
            get { return this.日時; }
            set
            {
                if (this.日時.HasValue)
                {
                    DateTime dt = new DateTime(
                        日時.Value.Year,
                        日時.Value.Month,
                        日時.Value.Day,
                        value.Value.Hour,
                        value.Value.Minute,
                        0);
                    this.日時 = dt;
                }
            }
        }
    }

ここで .NET6 への移植時に問題が発生します。

.NET Framework の EF では、テーブル構造が *.edmx ファイルに分離されているので、拡張した日時_年月日プロパティや日時_時分プロパティは更新時に無視されるのですが、.NET6 の EFCoreでは更新対象がEntityクラスの全プロパティとなるため、この拡張したプロパティを「無視」させるようにしなければなりません。

DbContext の OnModelCreating 内で、

            modelBuilder.Entity<単発アラーム>()
                .Ignore("日時_年月日")
                .Ignore("日時_時分");
            modelBuilder.Entity<周期アラーム>()
                .Ignore("周期月一週")
                .Ignore("周期毎年")
                .Ignore("周期毎月")
                .Ignore("周期毎週")
                .Ignore("周期月一曜日")
                ;

のように、拡張したプロパティを ignore するようにしていきます。この部分は、拡張したプロパティのほうに Ignore属性が付けられればよいのですが、無いようです。不便ですね。

問題はこのエラーの発生は、コンパイル時ではなく実行時にしか発生しないので、いちいち確かめないといけません。元の Entity クラスを拡張していると結構やっかいな問題です。

WindowsFormsHost を使う

.NET Framework の WPF アプリケーションでは WindowsFormsHost を使って、Windows フォームの各種コントロールを埋め込むことができます。

これは客先の「カレンダーのフォーマット/色合いを従来のものと同じにしてほしい」という要望を実現したものです。従来は ACCESS のカレンダーを使っているので、WPF カレンダーのものは使えません。

しかたがないので、ユーザーコントロールを作って WindowsFormsHost で埋め込みます。ユーザーコントロールにしたのは、あちこちの画面でこのカレンダーが出てくるので共通化するためです。

<Canvas Grid.Row="1" Grid.Column="0" x:Name="cv">
    <WindowsFormsHost FontSize="20" x:Name="host" >
        <wf:MonthCalendar 
            x:Name="cal" CalendarDimensions="3,1" DateSelected="MonthCalendar_DateSelected" />
    </WindowsFormsHost>
</Canvas>

実は、.NET6 では WindowsFormsHost の部分が使えなくて暫く悩んでいたのですが、

<UseWindowsForms>true</UseWindowsForms>

プロジェクトファイルに UseWindowsForms を設定して解決します。おそらく、.NET6 の WPF アプリケーションでツールボックスから WindowsFormsHost をドロップすると自動的に、UseWindowsForms が true になるはずです。

office.dll への参照を追加する

かなり悩んだのが次のところです。

MsoTriState が存在しないというエラーがでています。これは Excel の Shape を設定している部分なのですが、Excel COM を参照させてはいても、このエラーは取り除けません。おそらく Excel/Word に共通化されている Shape クラスは「office, Version=15.0.0.0」にあるようです。

さて、 「office, Version=15.0.0.0」 は一体どこにあるのでしょうか?

実は、.NET Framework のプロジェクトを見ると「アセンブリ」のタブのほうに「office 15.0.0.0」を参照しているところがあります。これは Office の COM ではなくて、.NET Framework が提供しているアセンブリのほうなんですね。

.NET6 では各種のアセンブリを個別で参照することはできない(自動的にアセンブリ参照が解決されるため)ので、明示的に office.dll を参照させます。

プロジェクトファイルに以下を記述しても ok です。

  <ItemGroup>
    <Reference Include="office">
      <HintPath>c:\Program Files (x86)\Microsoft Visual Studio\Shared\Visual Studio Tools for Office\PIA\Office15\Office.dll</HintPath>
    </Reference>
  </ItemGroup>

その他

その他 Excel COM の参照や Newtonsoft.Json、ClosedXML の参照は問題なく動いています。

EFCore の拡張プロパティの問題は、実際に動かしてみないとエラーが発生しないのでもう少し対処が必要です。

作業時間はおおむね1日がかりなので、移行としては十分可能なレベルですが、問題は FromSqlRaw への書き換えですかね。外部結合を使う場合、LINQ ではかなり手間なので生 SQL で組んでしまうのですが、プロジェクトによって数が多いと大変かと思います。

.NET6 にしておくと何が便利かと言うと TargetFramework を「net6.0-windows10.0.17763.0」にしておいて Windows Runtime(UWP や Windows App SDKの機能)が手軽に使えるようになります。まあ、業務用のデスクトップアプリなので、公開とかしないわけでそれほど利用価値があるわけではないのですが。ふりがな機能とか通知機能、連絡帳などが WinRT のほうに入っていたりします。

カテゴリー: 開発 | .NET Framework 4.7 から .NET6 への移行ポイント はコメントを受け付けていません

Exposure Notifications/COCOAのビーコンを収集して解析する

Google/Apple の提供する Exposure Notifications では BLE でビーコンを発信しています。内部的には 32 バイトなデータを Advertising/Broadcasting ってことになります。

コード自体は、openCACAO/CacaoBeacon: https://github.com/openCACAO/CacaoBeacon でビーコンの受信&収集コードを公開しています。CacaoBeaconMonitor が Android アプリです。iOS 版はありません。ENs では、ビーコンにある ID を受信するのですが、iOS の場合はこれが OS 側で塞がれているためです。なので、ビーコンのモニタリングとしては Android あるいは Windows を使います。

ノートパソコンを持ってうろうろするのは大変なので(定点観測ならばノートPCでも十分ですが)、Android のスマホを使います。

感染が拡大したので蒐集してみる

コード作ったのが4か月前で、去年の秋には東京都の感染者がぐんぐん減っている時期で、実験的なデータがとりずらい状態でした。で、まあ、とりあえず放置状態にしてあったのですが、第6波の勢いは尋常ではなく、現時点で東京都では2万/日をカウントしています。実に去年の秋の1000倍近い数字になってしまったわけですが。

COCOA自体の稼働率は、おそらく人口比で3割を超えています。通勤のような社会人に限っていれば、半分以上はあるっぽいです。 CacaoBeaconMonitor の場合は、COCOA から発信するビーコンを受けて RPI を保持していく Google/Apple の Exposure Notifications のクローンのような動作をさせているので、多分正確に収集ができます。

陽性者とのマッチングは別途、probetek で zip をダウンロードしてきて、SQL Server 上でクエリを書いています。

実測データ

2022年2月3日に、池袋を2時間ほど徘徊したデータ

https://1drv.ms/u/s!AmXmBbuizQkXgrlPo98iECzyoSgy8w?e=nr7QM0

上記から SQLite のデータがダウンロードできます。東武東上線で池袋まで行って、東武デパートで食事をして、帰ってくるまでの2時間のデータになります。

収集データの解説

テーブル構造は、以下の通り

CREATE TABLE RPI (
    Id INTEGER NOT NULL CONSTRAINT PK_RPI PRIMARY KEY AUTOINCREMENT,
    Key BLOB NULL,
    Metadata BLOB NULL,
    StartTime TEXT NOT NULL,
    EndTime TEXT NOT NULL,
    RssiMin INTEGER NOT NULL,
    RssiMax INTEGER NOT NULL,
    MAC INTEGER NOT NULL
)

ビーコンの受信開始(StartTime)と受信終了(EndTime)で接触時間が計測できます。ただし、ビーコンの受信データだけだと、発信のほうが10分で切り替わるので10分以内しか判別できません。別途、陽性者のTEKをダウンロードしてきて、受信したRPIを照合させると10分以上の接触が確認できます。実際の ENs が「接触の可能性あり」で通知しているのはこの部分です。

電波強度(RssiMax)を見ると、どれだけ近接しているかがわかります。私自身 iPhone に COCOA を入れているので、ひとつだけはかなり近いデータになっています。それ以外は、電車やホームでのすれ違い、エスカレーターで並んでいたときと考えられます。データの収集は一瞬でも近づけばデータとして保持されるので、きちんと解析するときは、近接している時間(EndTime – StartTime)と、電波強度(RssiMax)を見るとよいでしょう。

実際に実験をしてみると、バスが通ったり、電車が通過したときでも同時接続数が40程度の爆上がりします。いわゆる一瞬のすれ違いでも計測されるということです。

COCOA が使う ENs の仕組みは完全に公開されているとは言えませんが、おおまかなところはコードで示されています。実際の照合のところは数式でしか示されていないのが残念なところですが、公式で言われている通り、電波強度と近接時間の両方をみて判断して1mと15分以上という値を割り出しています。

TEK データのダウンロード

COCOA を通して陽性登録された TEK のデータを probetek コマンドでダウンロードします。

dotnet run --all

2/4時点で、50万件のTEKがあります。TEK は日単位で変わるので、おおむね7で割ると登録数になります。おおむね7万件というところでしょう。このデータは全国なので、2,3割の陽性者が登録している計算になります。実際のところは、厚生労働省のページ https://www.mhlw.go.jp/stf/seisakunitsuite/bunya/cocoa_00138.html で確認ができます。

この TEK データを SQL Server にアップロードします。

dotnet run --update-tek

TEK から、その日の 144個(10分単位)の RPI を生成して EXRPI テーブルに挿入しています。

と思ったのですが、データ量が 50万 x 144 で 7200万件になるので尋常ではありません。そのまま動かすとPCのメモリのデータベースもいっぱいになってしまうので、照合の部分は考え直しましょう。

実際の Google/Apple の ENs では C++ でバイナリ照合しているのでメモリは枯渇しません。どうやら照合用のツールを別途作る必要がありそうです。

カテゴリー: 開発 | Exposure Notifications/COCOAのビーコンを収集して解析する はコメントを受け付けていません

Redmine の docker を作る

Hyper-V + WLS2 な環境で Redmine を構築してもよいのですが、いろいろ面倒(特に Ruby on Rails が面倒)なので、docker を使って構築します。

Windows 上で構築すると MySQL が相乗りになったり、Ruby のパスを設定したりといろいろ面倒で、仮想環境の Linux に入れていたのですが、.NET6 + gRPC の実験用の構築です。

docker-compose を使え

その昔、ひとつのコンテナに ruby + mysql を入れてあれこれと構築していた頃があったのですが、結果的にひとつのコンテナにひとつのアプリを入れてしまったほうが楽ですね。

Redmine – Official Image | Docker Hub https://hub.docker.com/_/redmine/

なので、素直に docker-composer を使います。以前の Docker Desktop ではうまく動かないことが多かったのですが、現在は結構スムースっぽい。

以下を、docker-compose.yml というファイル名で保存します。

公式との違いは、

  • MySQL を外から参照できるようにポートフォワードしている(後で gRPC で使う)
  • command で絵文字が使えるように utf8mb4 を指定する。
    これがないと、Redmine で日本語を使ったときに落ちる。
version: '3.1'

services:

  redmine:
    image: redmine
    restart: always
    ports:
      - 8081:3000
    environment:
      REDMINE_DB_MYSQL: db
      REDMINE_DB_PASSWORD: example
      REDMINE_SECRET_KEY_BASE: supersecretkey

  db:
    image: mysql:5.7
    restart: always
    ports:
      - 33061:3306
    environment:
      MYSQL_ROOT_PASSWORD: example
      MYSQL_DATABASE: redmine
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

パスワードは面倒なので「example」のままで使っています。

この例では、

  • Redmine に localhost:8081 で接続
  • MySQL にポート番号 33061 で接続
  • mysql -u root -p で、パスワード example で接続できる

このあとで、docker-compose.yml と同じディレクトリで、以下を行えば ok

# コンテナの作成
docker-compose create
# コンテナの実行
docker-compose start

名前を間違ったりポートを間違ったりうまく作れなかったときは、Docker Desktop でコンテナを削除して何度も作り直します。

この例では、

こんな感じの2つのコンテナがひとつにまとめられます。

ブラウザで確認

localhost:8081 で確認します。初期パスワードは、admin/admin です。

初期データ(ロールやトラッカーなど)が入っていないので、デフォルト値のロードをします。このとき、文字コードが、utf8mb4 あるいは utf8 になっていないと、redmine 側でエラーになります。このときは、docker-compose の戻って、再構築します。

MySQL Workbench で確認

docker 内の mysql に接続します。

無事、テーブル構造が取得できています。

これを使って、.NET6 から CURD 用のエンティティクラスを作って、gRPC の *.proto をコード生成してやれば、適当なフロントエンドが作れるかも、ってところまで。

ちなみに、Oracle 19c の場合は 8GB 位メモリを持っていかれるのですが、redmine の場合は、512MB 位で済みます。これだとローカルPC内で動かしても大丈夫そう。oracle 19c のほうは、別のマシンに移動しました。

カテゴリー: 開発 | Redmine の docker を作る はコメントを受け付けていません

Oracle Database 19c な docker 環境の構築

基本的に Linux な環境で構築する場合 [Oracle Database] 公式Docker Imageを利用してOracle Database 19c環境を構築してみた | | IT Edge Blog https://itedge.stars.ne.jp/docker_image_oracle_database_19c/ と同じなのですが、Windows 10 の場合 image をどのように作るのか?の問題があります。

で、WLS2 で作ったらあっさりできたので、メモ代わりの残しておきます。

19c な Docker Image を作る

Oracle 公式では、12c https://hub.docker.com/_/oracle-database-enterprise-edition までしか用意されていません。結構古いものがのっかったままなので、どうするのかというと、Github にある Oracle の公式手順で作成していきます。

docker-images/OracleDatabase/SingleInstance at main · oracle/docker-images https://github.com/oracle/docker-images/tree/main/OracleDatabase/SingleInstance

ここの手順にしたがって、

$ ./buildContainerImage.sh

してから、

$ docker run –name

すればよいのです。

Linux 環境だと sh があるので、 buildContainerImage.sh が動くのですが、Windows 環境の場合はどうするのか?と言えば、WLS2 を使います。本当は Docker Image をバイナリで出力してくれればいいのですが、このスクリプトはそのまま docker に乗せてしまうので、さて、どうしたものかという感じではあるのですが。

大丈夫。WLS2 とホストな Windows 10(あるいは Windows 11)は内部的にはつながっているので、WLS2 から docker に乗せたもは Windows のほうの docker に乗せられます。というか、Windows のほうが WLS2 の docker を借用するようになっているのかもしれません。

まずは、git clone で oracle/docker-images を丸ごと取得

git clone git@github.com:oracle/docker-images.git

WLS2 を開いて、docker-images/OracleDatabase/SingleInstance/dockerfiles フォルダーへ移動

Oracle 本家から LINUX.X64_193000_db_home.zip をダウンロードして、19.3.0 に置く。zip は解凍せずにそのままでok。

例えば 19.c(19.3.0)で、Enterprise Edition を入れる場合は以下のように指定。

./buildDockerImage.sh -v 19.3.0 -e

これを実行すると、oracle/database の docker image が Windows のほうに追加されます。

コンテナを作る

コンテナを作るのは、Windows のほうで可能です。コマンドラインで次を実行します。

docker run --name oracle19c -p 1521:1521 -p 5500:5500 oracle/database:19.3.0-ee

ホストとなる Windows のほうでは Oracle が入っていないので、ポート番号は 1521 のままにしておきます。ホストとなる Windows のポート番号を変えておきたいときは -p 15210:1521 のようにします。Windows 側で localhost:15210 につなげたときに docker 側で 1521 につながるようになります。

docker run に渡すパラメータは以下の通りです。

docker run --name <container name> \
-p <host port>:1521 -p <host port>:5500 \
-e ORACLE_SID=<your SID> \
-e ORACLE_PDB=<your PDB name> \
-e ORACLE_PWD=<your database passwords> \
-e INIT_SGA_SIZE=<your database SGA memory in MB> \
-e INIT_PGA_SIZE=<your database PGA memory in MB> \
-e ORACLE_EDITION=<your database edition> \
-e ORACLE_CHARACTERSET=<your character set> \
-e ENABLE_ARCHIVELOG=true \
-v [<host mount point>:]/opt/oracle/oradata \
oracle/database:19.3.0-ee

面倒なので、コンテナを作成したのですが、気を付けるポイントとしては、

  • ORACLE_SID のデフォルトが「ORCLCDB」
  • ORACLE_PDBのデフォルトが「ORCLPDB1」
  • ORACLE_PWDは、コンテナの実行時に自動的に決まります。

sys で接続確認

Windows のほうに sqlplus のクライアントだけを入れておくと、接続確認ができます。

sqlplus sys/siosWOA6SKw=1@localhost:1521/ORCLCDB as sysdba

ログインユーザーと表領域の作成

ログインユーザーを作ってためしておきます。

あとで、.NET から接続確認したいので redmine ユーザーを作ります。

alter session set container=ORCLPDB1;

create tablespace "redminets" datafile '/opt/oracle/oradata/ORCLCDB/redminets.dbf' size 500M
 AUTOEXTEND ON NEXT 100M MAXSIZE 1G LOGGING EXTENT MANAGEMENT
 LOCAL SEGMENT SPACE MANAGEMENT AUTO;

create user redmine
 identified by redmine
 default tablespace "redminets"
 account unlock ;

GRANT connect TO "REDMINE";
GRANT CREATE SESSION TO "REDMINE";
GRANT "RESOURCE" TO "REDMINE";
ALTER USER "REDMINE" DEFAULT ROLE ALL;

あとは、sqlplus での接続の仕方が違うので、注意しておきましょう。SID とサービス名が初期値で少しややこしくなています。

□docker内で
sqlplus redmine/redmine@orclpdb1
sqlplus redmine/redmine@localhost:1521/ORCLCDB

□windows側から
sqlplus sys/siosWOA6SKw=1@localhost:1521/ORCLCDB as sysdba
sqlplus redmine/redmine@localhost:1521/orclpdb1

lsnrctl status の結果がこちら

メモリに注意

と、ここまで書きましたが、実は docker に oracle database 19c を入れるのはあまりお勧めできません。Oracle 公式が提供している docker images 作成用のスクリプトが非常に多くのメモリを利用する設定しく、コンテナを動かすと 16 GB ほど使われます。

docker stats で調べると 8GB 位つかっています。

こんなに使われると、ホスト側の Windows が死んでしまうので、.wslconfig で制限をします。この加減がよくわからないのですが、まあ、Hyper-V の仮想環境で Windows Server + Oracle を作ったときと同じくらい喰うのはどうなの?って感じです。環境的には、可搬性があるからいいけど。

Docker+WSL2の環境でVmmemのメモリ量が巨大になるのを制限する – いろいろ備忘録日記 https://devlights.hatenablog.com/entry/2021/10/28/073000

カテゴリー: 開発 | Oracle Database 19c な docker 環境の構築 はコメントを受け付けていません

Oracle Database 21c で LINQ を使う準備

Hyper-V の Windows Server に Oracle 21c を入れて、LINQ が使えるようにするところまでのメモ書きです。

Oracleは 11g 以来(実務だと9i あたりか)なので、途中にかなり間があいていますが、インストールとかデータベース作成とか落とし穴的なところは以前と変わらず。ちなみに、Java で作ってある超遅いインストーラは 21c から抜群に早くなっています。平行して試した 19c だと遅いままなので、おそらく 21c から最適されていた模様です。

おそらくお手軽なのは、Oracle Cloud を使うようなのですが、今回は訳あってオンプレミスな環境でテストをします。

仮想環境の構築

  • Hyper-V 上に Windows Server 2022 をインストール
  • メモリを 8GB、ストレージを 64GB 割り当て
  • ストレージは、ホストの SSD 上に作成する(スピードが2倍位違います)

動かしてみると常時 5GB 程度のメモリを使っているので、8GB 程度にするほうがよさそうです。ただし、Oracle 21c インストール時に「メモリが足りないけど続行するか?」のメッセージが出るので、余裕があれば 16GB 程度割り当てたほうがよさそう。が、実際のところ、仮想環境に 16GB 割り当てるのは難しそうなので、8GB 割り当てます。

Azure などのクラウド上で Windows Server を構築する場合、メモリが 8GB になると結構な課金が発生するので注意してください。4GB だと、インストール時に結構辛いかもしれません。

試験環境での実験なので、Express ではなく本家のものを使います。

Oracle Database 21c Download for Microsoft Windows x64 https://www.oracle.com/database/technologies/oracle21c-windows-downloads.html

以前は製品版を使うとライセンスサーバーを必要としていたのですが、今はどうなのだろう?

.NET6 の環境構築のための Visual Studio 2022 を入れます。

Visual Studio 2022 コミュニティ エディション – 最新の無料バージョンをダウンロードする https://visualstudio.microsoft.com/ja/vs/community/

dotnet & Visual Studio Code で軽量化することも可能ですが、NuGet の扱いとかデバッグ出力とかの扱いで面倒がないので VS2022 を入れてします。

Oracle Database 21c のインストールと初期設定

実は、Oracle 21c からプラカブルデータベースが必須になりました。インストールの選択に非PDBの選択肢がなくなっています。

  • CDB:コンテナデータベース
  • PDB:プラカブルデータベース

Oracle の場合、「データベース」という名の付くものが色々あって結構ややこしいです。

SQL Server の場合、インスタンス > データベース >テーブルのように比較的単純なのですが、

Oracle の場合、グローバルデータベース(インスタンス)>コンテナ(コンテナデータベース)>プラカブデータベース>テーブル という形で、いたるところで「データベース」が出てきます。

以前、あったテーブルスペース(表領域)はどこにいったのでしょう?という疑問があるのですが、これは後で調べます。スクリプトでデータベース(プラカブデータベース)を作り限り、従来通りの TABLESPACE も使えているようです。

インストール時に「ソフトウェアのみインストールする」にして、あとからデータベースを作成しています。

  • グローバルデータベース:orcl.mshome.net
  • プラカブルデータベース:orcldb

sqlplus / as sysdb で、次の状態まで確認します。

ORALCE_BASE と ORACLE_HOME

ORACLE_BASE を c:\app\oracle にして作成したのですが、tnsnames.ora 等のファイル位置が 19c と 21c では異なっています。ORACLE_HOME に下にはありません。

tnsnames.ora 等のファイルは以下にあります。

C:\app\oracle\homes\OraDB21Home1\network\admin

どうやら、$ORACLE_BASE/homes 配下に作成されるようです。

以下の例では、

  • ホスト名:oracle21sv
  • Oracle で設定される?サーバー名:oracle21sv.mshome.net
  • サービス名:orcldb.mshome.net
  • システム識別子(SID):ORCL

となっています。

listener.ora

# listener.ora Network Configuration File: C:\app\oracle\homes\OraDB21Home1\NETWORK\ADMIN\listener.ora
# Generated by Oracle configuration tools.

SID_LIST_LISTENER =
  (SID_LIST =
    (SID_DESC =
      (SID_NAME = CLRExtProc)
      (ORACLE_HOME = C:\app\oracle\product\21.3.0\dbhome_1)
      (PROGRAM = extproc)
      (ENVS = "EXTPROC_DLLS=ONLY:C:\app\oracle\product\21.3.0\dbhome_1\bin\oraclr.dll")
    )
  )

LISTENER =
  (DESCRIPTION_LIST =
    (DESCRIPTION =
      (ADDRESS = (PROTOCOL = TCP)(HOST = oracle21sv.mshome.net)(PORT = 1521))
      (ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC1521))
    )
  )

tnsnames.ora

# tnsnames.ora Network Configuration File: C:\app\oracle\homes\OraDB21Home1\NETWORK\ADMIN\tnsnames.ora
# Generated by Oracle configuration tools.

ORACLR_CONNECTION_DATA =
  (DESCRIPTION =
    (ADDRESS_LIST =
      (ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC1521))
    )
    (CONNECT_DATA =
      (SID = CLRExtProc)
      (PRESENTATION = RO)
    )
  )

ORCL =
  (DESCRIPTION =
    (ADDRESS_LIST =
      (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))
    )
    (CONNECT_DATA =
      (SERVICE_NAME = orcldb.mshome.net)
    )
  )

このあたりの設定がややこしいので、

  • lsnrctl status
  • tnsping <システム識別子(SID)>

を活用して事前にチェックしておきます。

うまく TNS の設定ができていれば tnsping orcl がで tnsnames.ora の設定が取得できます。

確か、 listener.ora と tnsnames.ora を手作業で修正した場合は、lsnrctl を再起動させます。

  • lsnrctl start
  • lsnrctl stop

データベースの作成

昔ながらのテーブルスペースを使った方式です。これいいかわからないので、これは後で修正。

CREATE TABLESPACE REDMINETS DATAFILE 
 'C:\app\oracle\oradata\ORCL\DATAFILE\REDMINETS.dbf' 
  SIZE 1G AUTOEXTEND ON NEXT 100M MAXSIZE UNLIMITED;

CREATE USER redmine IDENTIFIED BY redmine 
  DEFAULT TABLESPACE REDMINETS 
  TEMPORARY TABLESPACE TEMP 
  ACCOUNT UNLOCK ;

GRANT UNLIMITED TABLESPACE TO redmine;
GRANT CREATE SESSION TO redmine;
GRANT CONNECT TO redmine;
GRANT RESOURCE TO redmine;
ALTER USER "REDMINE" DEFAULT ROLE ALL;

redmine という名前でユーザーを作成しておきます。

  • 表領域 REDMINETS
  • ユーザー名 redmine(内部では自動的に大文字になるので、正確には REDMINE です)
  • パスワード redmine

権限が少し過剰ですが、ひとまずこれで Ok です。この使い方は、いまとなっては少しイリーガルなので、プラカブルデータベースを使った方式に直します。

この状態で、sqlplus でログインできるようになります。

sqlplus redmine/redmine@orcl

最後の orcl は、tnsnames.ora に設定したシステム識別子(SID)を使います。

tnsnames.ora が間違っている場合は、「ORA-12154: TNS: 指定された接続識別子を解決できませんでした」のエラーがでます。

ユーザー名あるいはパスワードが間違っている場合は「ORA-01017: invalid username/password; logon denied」です。

ほかにも、サービス名が間違っている、サーバー名が間違っている、場合もあるので、lsnrctl status と tnsping を使ってチェックしていきます。

テーブルの作成

テスト用の Books テーブルを作成します。

create table books (
  id number(16) not null,
  title varchar2(20),
  author varchar2(20),
  publisher varchar2(20)
);

仮データの挿入

insert into books values ( 1, 'Oracle入門', 'A', '出版社' );
insert into books values ( 2, 'SQL Server入門', 'B', '出版社' );
insert into books values ( 3, 'MySQL入門', 'A', '同人誌' );

テストプロジェクトの作成

接続確認をするプロジェクトを作成します。

dotnet new console --name DbConnectSample

NuGetで、ライブラリを追加

  • Microsoft.EntityFrameworkCore
  • Oracle.EntityFrameworkCore ODP.NET EF Core
  • Oracle.ManagedDataAccess.Core ODP.NET Core
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
	<ItemGroup>
		<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.12" />
		<PackageReference Include="Oracle.EntityFrameworkCore" Version="5.21.4" />
		<PackageReference Include="Oracle.ManagedDataAccess.Core" Version="3.21.4" />
	</ItemGroup>
</Project>

Oracle.EntityFrameworkCore と Microsoft.EntityFrameworkCore のバージョンを 5 で揃えておきます。これは現時点では、 Oracle.EntityFrameworkCore は EF Core 5 までしか対応していないためです。

Oracle.ManagedDataAccess.Core は Oracle への接続のために使っています。

実験コードは以下の通りです。

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Oracle.ManagedDataAccess.Client;

Console.WriteLine("Hello, Oracle World!");
Console.WriteLine("use DbContext");
var db = new OracleDbContext();
var items = db.Books.ToList();
foreach (var it in items)
{
    Console.WriteLine($"{it.ID} {it.TITLE}");
}

public class OracleDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var builder = new OracleConnectionStringBuilder();
        builder.UserID = "redmine";
        builder.Password = "redmine";
        builder.TnsAdmin = @"C:\app\oracle\homes\OraDB21Home1\network\admin";
        builder.DataSource = "ORCL";
        string cnstr = builder.ConnectionString;
        optionsBuilder.UseOracle(cnstr);
    }
    public DbSet<BOOKS> Books => Set<BOOKS>();
}

/*
SQL> desc books ;
 名前                                                  NULL?    型
 ----------------------------------------------------- -------- ------------------------------------
 ID                                        NOT NULL NUMBER(16)
 TITLE                                              VARCHAR2(20)
 AUTHOR                                             VARCHAR2(20)
 PUBLISHER                                          VARCHAR2(20)
 
 */
[Table("BOOKS")]    // これが必須
public class BOOKS
{
    [Key]
    public Int64 ID { get; set; }
    public string TITLE { get; set; } = "";
    public string AUTHOR { get; set; } = "";
    public string PUBLISHER { get; set; } = "";
}

Oracle のほうではテーブル名やカラム名を大文字で保持しているようなので、エンティティクラスのほうも大文字にしておきます。ただし、テーブル名はなぜかそのままでは認識できず Table 属性で指定します(おそらく ODP.NET EF Core のほうの不具合ような気も)。

実行結果

大文字のまま扱ってもよいのですが、C# の命名規約上のちのちややこしいことになるので、Column 属性で変更しておきます。

[Table("BOOKS")]
public class Books
{
    [Key]
    [Column("ID")] public Int64 Id { get; set; }
    [Column("TITLE")] public string Title { get; set; } = "";
    [Column("AUTHOR")] public string Author { get; set; } = "";
    [Column("PUBLISHER")] public string Publisher { get; set; } = "";
}

これでうまくC#の命名規則にあわせられます。

カテゴリー: 開発 | Oracle Database 21c で LINQ を使う準備 はコメントを受け付けていません

既存のMySQLからEF CoreのEntityを作成する

MySQLの場合、コードファースト的にコードからデータベースを更新していけばよいのですが、既存のデータベースから Entity を作りたいときがあります。まあ、先日てもとの Redmine をバージョンアップさせて、せっかくなので .NET6 からアクセスしようと思って、さて、と思案した経過を残しておきます。

redmine.projects のエンティティクラスを手作業で作る

一番手っ取り早いのは、手作業でエンティティクラスを作ることです。エンティティクラスは単純な値クラスなので、プロパティを並べれば ok.

MySQL Workbench の結果から、ちまちまと C# のクラスを作るか、スキーマを参照しながら手作業で作ります。

CREATE TABLE `projects` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL DEFAULT '',
  `description` text,
  `homepage` varchar(255) DEFAULT '',
  `is_public` tinyint(1) NOT NULL DEFAULT '1',
  `parent_id` int DEFAULT NULL,
  `created_on` timestamp NULL DEFAULT NULL,
  `updated_on` timestamp NULL DEFAULT NULL,
  `identifier` varchar(255) DEFAULT NULL,
  `status` int NOT NULL DEFAULT '1',
  `lft` int DEFAULT NULL,
  `rgt` int DEFAULT NULL,
  `inherit_members` tinyint(1) NOT NULL DEFAULT '0',
  `default_version_id` int DEFAULT NULL,
  `default_assigned_to_id` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_projects_on_lft` (`lft`),
  KEY `index_projects_on_rgt` (`rgt`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 ;
  • NULL 許可の部分をチェック
  • tinyint(1) を bool に直す
  • timestamp を DateTime あるいは DateTimeOffset に直す
  • 主キーに Key 属性をつける

部分を注意すれば比較的簡単に C# のエンティティクラスができます。

public class projects
{
    [Key]
    public int id { get; set; }
    public string name { get; set; } = "";
    public string? description { get; set; }
    public string? homepage { get; set; }
    public bool is_public { get; set; }
    public int parent_id { get; set; }
    public DateTime created_on { get; set; }
    public DateTime updated_on { get; set; }
    public string identifier { get; set; } = "";
    public bool status { get; set; }
    public int? lft { get; set; }
    public int? rgt { get; set; }
    public int inherit_members { get; set; }
    public int? default_version_id { get; set; }
    public int? default_assigned_to_id { get; set; }
}

個人的なプロジェクトならば、これで十分です(自分で趣味的に Redmine を扱うにもこれで十分)。

しかし、命名規則が C# にあっていないために大量に警告が出ます。さらに、仕事で使う場合にはプロパティ名自体が開発プロジェクト全体で使うために、あちこちに MySQL 特有のケバブケース(アンダースコアを使う命名法)が散らかってしまいます。たとえば、is_public ではなくて IsPublic にしておきたいわけで、ここで MySQL のカラム名と C# のプロパティ名の変換が発生します。

名前の変換は System.ComponentModel.DataAnnotations.Schema にある属性を使って切り替えます。

using System.ComponentModel.DataAnnotations.Schema;

[Table("proejcts")]
public class Project
{
...
    [Column("is_public")]
    public bool IsPublic { get; set; }
...
}

Table 属性と Column 属性をちまちまと指定していけば、

  • MySQL 側のカラム名を Column で指定
  • プロパティ名は C# の命名規約に合わせる

ことができます。ちなみに MySQL の設定によってはテーブル名やカラム名の大文字小文字が区別されるため、環境にそろえようとすると(特に Linux上)、この属性は必須になります。

これを DbContext を継承したクラスに設定すれば、自由に LINQ が使えるようになります。

public class RedmineDataContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var builder = new MySqlConnectionStringBuilder();
        builder.Server = "localhost";
        builder.Database = "redmine";
        builder.UserID = "redmine";
        builder.Password = "redmine";
        optionsBuilder.UseMySQL(builder.ConnectionString);
    }
    public DbSet<Project> Project => Set<Project>();
}

さて、問題はここから、この手作業で作ったエンティティクラスをどのように大量に作っているのか?を考えます。数個のテーブルならば手作業で作ってよいのですが、数十個になると結構大変。100以上になるとちょっと手作業では太刀打ちできない。しかも、いつの間にかデータベースのほうから更新する場合に再び手作業に変えていくのはもっと大変な作業です。

コードファースト的にコードからデータベースを更新(マイグレーション)していけばいいのですが、既存のDBを扱う場合にはなかなかコードファーストという訳にはいきません。

テーブル構成を取得する

結論から言いますが、テーブル構成をちまちま取得するよりも、「dotnet ef dbcontext scaffold」でエンティティクラスを一気に作ったほうが楽です。dontet の scaffold は MySQL に正式には対応していないようなのですが、一応は動きます。

desc で取得する

MySQL のコマンドラインから desc <テーブル名> で取得が可能です。

MariaDB [redmine]> desc projects ;
+------------------------+--------------+------+-----+---------+----------------+
| Field                  | Type         | Null | Key | Default | Extra          |
+------------------------+--------------+------+-----+---------+----------------+
| id                     | int(11)      | NO   | PRI | NULL    | auto_increment |
| name                   | varchar(255) | NO   |     |         |                |
| description            | text         | YES  |     | NULL    |                |
| homepage               | varchar(255) | YES  |     |         |                |
| is_public              | tinyint(1)   | NO   |     | 1       |                |
| parent_id              | int(11)      | YES  |     | NULL    |                |
| created_on             | timestamp    | YES  |     | NULL    |                |
| updated_on             | timestamp    | YES  |     | NULL    |                |
| identifier             | varchar(255) | YES  |     | NULL    |                |
| status                 | int(11)      | NO   |     | 1       |                |
| lft                    | int(11)      | YES  | MUL | NULL    |                |
| rgt                    | int(11)      | YES  | MUL | NULL    |                |
| inherit_members        | tinyint(1)   | NO   |     | 0       |                |
| default_version_id     | int(11)      | YES  |     | NULL    |                |
| default_assigned_to_id | int(11)      | YES  |     | NULL    |                |
+------------------------+--------------+------+-----+---------+----------------+
15 rows in set (0.01 sec)

MariaDB [redmine]>

これをテキストに落としてからちまちま適当なスクリプトでコンバートしてもよいのですが、実は FromSqlRaw メソッドを使って直接 SQL 文を実行しても取得できます。

public class DescResult
{
    [Key]
    public string Field {  get; set; }
    public string Type { get; set; }
    public string Null { get; set; }
    public string? Key { get; set; }
    public string? Default { get; set; }
    public string? Extra { get ; set; }
}

public class RedmineDataContext : DbContext
{
...
    public DbSet<DescResult> DescResult => Set<DescResult>();
}

var cnn = context.Database.GetDbConnection() as MySqlConnection;
var result = context.DescResult.FromSqlRaw("DESC projects");
Console.WriteLine("\nDESC projects");
Console.WriteLine("Field    Type    Null    Key Default Extra");
foreach ( var it in result )
{
    Console.WriteLine($"{it.Field}\t{it.Type}\t{it.Null}\t{it.Key}\t{it.Default}\t{it.Extra}");
}

DbContext に DescResult のように結果を含めるクラスが必要になるのが不満ではありますが、こんな風に取れます。

この中身をチェックして自前でエンティティクラスを取得してもよいでしょう。

GetSchema メソッドを使う

実は DbConnection クラスには GetSchema というスキーマ(テーブル構成)を取得するための便利なメソッドがついています。かつて VB で使っていた ADO に対する ADOX のようなもので、対象となるデータベースの構成情報が取得できます。これは、SQL Server に限らず、MySQL でも取れるのでこれを活用できます。

neue cc – Micro-ORMとテーブルのクラス定義自動生成について http://neue.cc/2013/06/30_411.html

を参考にしながら作っていきましょう。いまとなっては dynamic だと辛いので、エンティティクラスを作ります。

ところで、GetSchema に渡すテーブル名?はデータベースそれぞれになるので、Oracle とか SQL Server の場合は独自に探さないといけません。他のデータベースはさておき、MySQL の場合は、GetSchema(“COLUMNS”) で構成情報が取得できます。

エンティティクラスを作成するために(仕方がなく手作業になりますが)、列名の詳細が必要になります。

MySQL :: MySQL 8.0 Reference Manual :: 26.3.8 The INFORMATION_SCHEMA COLUMNS Table https://dev.mysql.com/doc/refman/8.0/en/information-schema-columns-table.html

MySQL のマニュアルを参照しながら、ちまちまと作ってもよいのですが、desc コマンドでも取れます。

MariaDB [redmine]> desc INFORMATION_SCHEMA.COLUMNS ;
+--------------------------+---------------------+------+-----+---------+-------+
| Field                    | Type                | Null | Key | Default | Extra |
+--------------------------+---------------------+------+-----+---------+-------+
| TABLE_CATALOG            | varchar(512)        | NO   |     |         |       |
| TABLE_SCHEMA             | varchar(64)         | NO   |     |         |       |
| TABLE_NAME               | varchar(64)         | NO   |     |         |       |
| COLUMN_NAME              | varchar(64)         | NO   |     |         |       |
| ORDINAL_POSITION         | bigint(21) unsigned | NO   |     | 0       |       |
| COLUMN_DEFAULT           | longtext            | YES  |     | NULL    |       |
| IS_NULLABLE              | varchar(3)          | NO   |     |         |       |
| DATA_TYPE                | varchar(64)         | NO   |     |         |       |
| CHARACTER_MAXIMUM_LENGTH | bigint(21) unsigned | YES  |     | NULL    |       |
| CHARACTER_OCTET_LENGTH   | bigint(21) unsigned | YES  |     | NULL    |       |
| NUMERIC_PRECISION        | bigint(21) unsigned | YES  |     | NULL    |       |
| NUMERIC_SCALE            | bigint(21) unsigned | YES  |     | NULL    |       |
| DATETIME_PRECISION       | bigint(21) unsigned | YES  |     | NULL    |       |
| CHARACTER_SET_NAME       | varchar(32)         | YES  |     | NULL    |       |
| COLLATION_NAME           | varchar(32)         | YES  |     | NULL    |       |
| COLUMN_TYPE              | longtext            | NO   |     | NULL    |       |
| COLUMN_KEY               | varchar(3)          | NO   |     |         |       |
| EXTRA                    | varchar(27)         | NO   |     |         |       |
| PRIVILEGES               | varchar(80)         | NO   |     |         |       |
| COLUMN_COMMENT           | varchar(1024)       | NO   |     |         |       |
+--------------------------+---------------------+------+-----+---------+-------+
20 rows in set (0.01 sec)
  • bigint(21) を System.UInt64 に変換

の部分を注意して、エンティティクラスを作ります。

[Table("COLUMNS", Schema = "INFORMATION_SCHEMA")]
public class MySqlColumns
{
    public string TABLE_CATALOG { get; set; } = "";
    public string TABLE_SCHEMA { get; set; } = "";
    public string TABLE_NAME { get; set; } = "";
    public string COLUMN_NAME { get; set; } = "";
    public System.UInt64 ORDINAL_POSITION { get; set; }
    public string? COLUMN_DEFAULT { get; set; }
    public string IS_NULLABLE { get; set; } = "";
    public string DATA_TYPE { get; set; } = "";
    public System.UInt64? CHARACTER_MAXIMUM_LENGTH { get; set; }
    public System.UInt64? NUMERIC_PRECISION { get; set; } = null;
    public System.UInt64? NUMERIC_SCALE { get; set; } = null;
    public System.UInt64? DATETIME_PRECISION { get; set; } = null;
    public string? CHARACTER_SET_NAME { get; set; } = null;
    public string? COLLATION_NAME { get; set; } = "";
    public string? COLUMN_TYPE { get; set; } = null;
    public string COLUMN_KEY { get; set; } = "";
    public string EXTRA { get; set; } = "";
    public string PRIVILEGES { get; set; } = "";
    public string COLUMN_COMMENT { get; set; } = "";
}

これで、直接 INFORMATION_SCHEMA.COLUMNS を呼び出すか、 GetSchema(“COLUMNS”) を呼び出して結果を LINQ で検索すればお手軽ですね。と思ったのですが、落とし穴があります。

  • なぜか MySQL の場合は、Table 属性で Schema を切り替えてくれない。
  • GetSchema の戻り値が、DataTable になっている。

まずは、DataTable の中身をチェックします。

Console.WriteLine("MySQL GetSchema");
var context = new RedmineDataContext();
var cn = context.Database.GetDbConnection();
cn!.Open();
var t = cn.GetSchema("columns");

Console.WriteLine("columns.列名");
foreach (DataColumn col in t.Columns)
{
    Console.WriteLine("{0} {1}", col.ColumnName, col.DataType);
}

Console.WriteLine("\ncolumns.rows");
Console.WriteLine("TABLE_NAME   COLUMN_NAME COLUMN_TYPE IS_NULLABLE COLUMN_KEY  EXTRA");

foreach ( DataRow row in t.Rows )
{
    Console.WriteLine("{0}  {1} {2} {3} {4} {5} {6}",
        row["TABLE_NAME"],
        row["COLUMN_NAME"],
        row["COLUMN_TYPE"],
        row["IS_NULLABLE"],
        row["COLUMN_KEY"],
        row["COLUMN_DEFAULT"],
        row["EXTRA"]);
}

columns のテーブル構造は、DataTable#Columns を使っても確認ができます。DataRow にいちいちカラム名を指定するのがアレですが、ひとまず DESC コマンド互換のものが取得できます。

DataTable からエンティティへ変換する拡張を作る

DataTable.Rows からエンティティに変換させる拡張メソッドを作ります。

public static class DataTableExtenstions
{
    /// <summary>
    /// DataTable.Rows を指定した List<T>に変換する
    /// </summary>
    public static List<T> ToList<T>(this DataTable src) where T : new()  
    {
        var items = new List<T>();
        var properties = typeof(T).GetProperties();
        // TODO: Column 属性があれば、探索するカラム名を変更する
        foreach ( DataRow row in src.Rows )
        {
            var item = new T();
            foreach ( var pi in properties )
            {
                var value = row[pi.Name];
                if ( value == System.DBNull.Value )
                {
                    pi.SetValue(item, null);
                } 
                else
                {
                    pi.SetValue(item, row[pi.Name]);
                }
            }
            items.Add(item);
        }
        return items;
    }
}

本当は Column 属性を調べないとダメなのですが、これは後日。ひとまず、詰め込みをできる ToList メソッドを作ったので、これを使って LINQ で使えるようにします。

var items = cn.GetSchema("columns").ToList<MySqlColumns>();
Console.WriteLine("\nMySqlColumns");
Console.WriteLine("TABLE_NAME   COLUMN_NAME COLUMN_TYPE IS_NULLABLE COLUMN_KEY  EXTRA");
items = items.OrderBy( t => t.TABLE_NAME).ThenBy( t => t.ORDINAL_POSITION ).ToList();

foreach (var it in items)
{
    Console.WriteLine($"{it.TABLE_NAME}  {it.COLUMN_NAME} {it.COLUMN_TYPE} {it.IS_NULLABLE} {it.COLUMN_KEY} {it.COLUMN_DEFAULT} {it.EXTRA}");
}

GetSchema の戻り値は、テーブル名等でソートされていないので、ソートしておきます。

結果は DESC コマンドと似た感じで取得できます。多分、DESC コマンドのほうは整形しているのでしょう。

これを使って T4 で変換してもよいし、別途コマンドツールを使って Models 群を作ります。が、ここで再び dotnet ef dbcontext scaffold に立ち戻って、もうちょっとスイッチを使ったら使いやすいように出ないか?(特に命名規約の部分)を調べ直します。

dotnet ef dbcontext scaffold を使う

使い方が下記のページに書いてあります。

リバース エンジニアリング – EF Core | Microsoft Docs https://docs.microsoft.com/ja-jp/ef/core/managing-schemas/scaffolding?tabs=dotnet-core-cli

書いてはあるのですが、実に使いにくいのです。dotnet ef コマンド自体がコードファーストのためにツールという感じがするので、あまり既存のデータベースから細かく取得することができません。まあ、それでもおおざっぱに取れればいいので、以下のコマンドを powershell で実行します。

コマンドを実行するときは、

  • Microsoft.EntityFrameworkCore.Design を含んだプロジェクトであること

が必須です。もともと webapi や mvc プロジェクトにエンティティクラスを追加するコマンドなので、このような形になっています。

dotnet ef dbcontext scaffold `
 "Server=localhost;User=redmine;Password=redmine;Database=redmine" `
 "MySql.EntityFrameworkCore" `
 --data-annotations `
 -o Models `
 --table projects `
 --force
  • `(バッククォート)は継続文字です
  • 接続文字列をそのまま記述します(これが結構面倒)
  • 接続するためのアセンブリを指定。MySQL の場合は、MySql.EntityFrameworkCore
  • –data-annotations C# と MySQL のカラム名の変換を属性で指定する
  • Models フォルダーに出力(これがないと、カレントフォルダーにばらまかれる)
  • –table スイッチで出力するテーブルを指定。指定しないと全テーブルが対象になる。
  • –force 上書き用のスイッチ

–data-annotations が肝で、デフォルトでは、DbContext の OnModelCreating にばらばらと記述されるのですが、これをエンティティクラスの属性に切り替えます。

これがうまくいくと Models/Project.cs が出力されます。この部分は、ASP.NET MVC 本とか Blazor本とかで必ず出てくるのですが、dotnet ef コマンドの扱いが難しくて、躓きやすいところですよね。なんとか乗り越えてください。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

#nullable disable

namespace getschema.Models
{
    [Table("projects")]
    [Index(nameof(Lft), Name = "index_projects_on_lft")]
    [Index(nameof(Rgt), Name = "index_projects_on_rgt")]
    public partial class Project
    {
        [Key]
        [Column("id", TypeName = "int(11)")]
        public int Id { get; set; }
        [Required]
        [Column("name")]
        [StringLength(255)]
        public string Name { get; set; }
        [Column("description")]
        public string Description { get; set; }
        [Column("homepage")]
        [StringLength(255)]
        public string Homepage { get; set; }
        [Required]
        [Column("is_public")]
        public bool? IsPublic { get; set; }
        [Column("parent_id", TypeName = "int(11)")]
        public int? ParentId { get; set; }
        [Column("identifier")]
        [StringLength(255)]
        public string Identifier { get; set; }
        [Column("status", TypeName = "int(11)")]
        public int Status { get; set; }
        [Column("lft", TypeName = "int(11)")]
        public int? Lft { get; set; }
        [Column("rgt", TypeName = "int(11)")]
        public int? Rgt { get; set; }
        [Column("inherit_members")]
        public bool InheritMembers { get; set; }
        [Column("default_version_id", TypeName = "int(11)")]
        public int? DefaultVersionId { get; set; }
        [Column("default_assigned_to_id", TypeName = "int(11)")]
        public int? DefaultAssignedToId { get; set; }
    }
}

Visual Studio 2022 ではデフォルトで nullable enable となっているので、警告をおさえるために「#nullable disable」が追加になっています。

すべてのカラム(プロパティ)にColumn属性がついているので、MySQLとC#のマッピングが出来た状態です。

これでおおむねはいけますね。

timestamp のカラムが消されているので注意

スキャフォードしたときに警告が出ているのですが、先の Project クラスを見ると、created_on と updated_on に相当するものがありません。実は、この2つのカラムの型は「timestamp」となっているので、C# の型に自動でコンバートしないため、削除されてしまっているのです。

仕方がないので、

  • timestamp を DatTime に変換

したプロパティを手作業で追加します。

MySQL の場合は、時間絡みの型がいくつかってそれをC#にどのようにコンバートするのか、日付の 0000/00/00 はどう扱うのか、が問題になるので注意しなければいけないところです。

でもって、やっとこさエンティティクラスの出力の目途が立ってので、これ gRPC で利用します。これは後日。

Pomelo.EntityFrameworkCore.MySql を使う

追記で、

スキャフォードを使うときに、Pomelo.EntityFrameworkCore.MySql を使うと timestamp の問題が解決できます。

PomeloFoundation/Pomelo.EntityFrameworkCore.MySql: Entity Framework Core provider for MySQL and MariaDB built on top of MySqlConnector https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql

dotnet ef dbcontext scaffold `
 "Server=localhost;User=redmine;Password=redmine;Database=redmine" `
 "Pomelo.EntityFrameworkCore.MySql" `
 --data-annotations `
 -o Models `
 --table projects `
 --force

この状態で project.cs が以下のように出力されます。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace getschema.Models
{
    [Table("projects")]
    [Index(nameof(Lft), Name = "index_projects_on_lft")]
    [Index(nameof(Rgt), Name = "index_projects_on_rgt")]
    [MySqlCharSet("utf8")]
    [MySqlCollation("utf8_general_ci")]
    public partial class Project
    {
        [Key]
        [Column("id", TypeName = "int(11)")]
        public int Id { get; set; }
        [Required]
        [Column("name")]
        [StringLength(255)]
        public string Name { get; set; }
        [Column("description", TypeName = "text")]
        public string Description { get; set; }
        [Column("homepage")]
        [StringLength(255)]
        public string Homepage { get; set; }
        [Required]
        [Column("is_public")]
        public bool? IsPublic { get; set; }
        [Column("parent_id", TypeName = "int(11)")]
        public int? ParentId { get; set; }
        [Column("created_on", TypeName = "timestamp")]
        public DateTime? CreatedOn { get; set; }
        [Column("updated_on", TypeName = "timestamp")]
        public DateTime? UpdatedOn { get; set; }
        [Column("identifier")]
        [StringLength(255)]
        public string Identifier { get; set; }
        [Column("status", TypeName = "int(11)")]
        public int Status { get; set; }
        [Column("lft", TypeName = "int(11)")]
        public int? Lft { get; set; }
        [Column("rgt", TypeName = "int(11)")]
        public int? Rgt { get; set; }
        [Column("inherit_members")]
        public bool InheritMembers { get; set; }
        [Column("default_version_id", TypeName = "int(11)")]
        public int? DefaultVersionId { get; set; }
        [Column("default_assigned_to_id", TypeName = "int(11)")]
        public int? DefaultAssignedToId { get; set; }
    }
}
  • #nullable disable は手作業で加える。

カテゴリー: 開発 | 既存のMySQLからEF CoreのEntityを作成する はコメントを受け付けていません

ACCESS を .NET Core の EF Coreでアクセスする

いまさら ACCESS を扱うにしても ADO.NET の DataSet や DataTable を使いたくない場合があります。素直に .NET Framework のほうを使ってもいいのですが、.NET5(多分 .NET6でも大丈夫)で ACCESS のデータベースファイル(*.mdb)を扱います。

EntityFrameworkCore.Jet: Entity Framework Core provider for Access database を使います。

ちょっとだけ難点があって、EF Core の 3.x(最新は EF Core 5.x)だけ対応しています。なので EF Core のバージョンをひとつ前に戻さないといけないのですが、DataSet を使ってちまちまとカラム名を入力するよりは楽です。現在のプロジェクトだと以下のようになります。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
	<ItemGroup>
		<PackageReference Include="EntityFrameworkCore.Jet.OleDb" Version="3.1.0" />
		<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.20" />
	</ItemGroup>
</Project>

これは WPF のプロジェクトなのですが、webapi も作れます。

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0-windows</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
	<PackageReference Include="EntityFrameworkCore.Jet.OleDb" Version="3.1.0" />
	<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.20" />
  </ItemGroup>
</Project>

webapi の場合、.NET5 で作ると TargetFramework が「net5.0」となっていますが「net5.0-windows」のように書き換えます。OLEDB 関係が windows 上のみの動作のため EntityFrameworkCore.Jet.OleDb の制限です。ちなみに net5.0 のままでも、警告はでますが EntityFrameworkCore.Jet.OleDb のクラスやメソッドは利用できます。

Microsoft.ACE.OLEDB.12.0 をインストール

実は Windows 10 が素の状態だと、ACCESS 用の OLE ドライバーが入ってません。あと、最新版の ACCESS の場合、Microsoft.ACE.OLEDB.16.0 だと、EntityFrameworkCore.Jet.OleDb がうまく動かなかったので 12.0 のほうを入れます。

Download Microsoft Access データベース エンジン 2010 再頒布可能コンポーネント from Official Microsoft Download Center
https://www.microsoft.com/ja-jp/download/details.aspx?id=13255

うちの Windows 10 の環境では 32bit 版の Office 2019 が入っていて、あとから Microsoft.ACE.OLEDB.16.0 の再配布コンポーネントがなぜか 32/64 のどちらも入れることができません。16.0 のほうは微妙にややこしいことになるかも。Microsoft.ACE.OLEDB.16.0のインストールについて

DbContext を作る

データベースにアクセスするための DbContext を作ります。これは EF Core の DbContext の作り方と同じです。OnModelCreating をオーバーライドして、HasNoKey を設定しているのは ACCESS データから読み取りしかしないからです。元テーブル(予約テーブルとかには Key が入っていません)。

public class AccessDbContext : DbContext
{
    public AccessDbContext()
    {

    }
    public AccessDbContext(DbContextOptions<AccessDbContext> options) : base(options)
    {

    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        OleDbConnectionStringBuilder builder = new OleDbConnectionStringBuilder();
        builder.Provider = "Microsoft.ACE.OLEDB.12.0";
        builder.DataSource = @"ACCESSファイル.mdb";
        string cnstr = builder.ConnectionString;
        optionsBuilder.UseJetOleDb(cnstr);
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<T_予約>().HasNoKey();
        modelBuilder.Entity<T_予約sub>().HasNoKey();
        modelBuilder.Entity<T_顧客>().HasNoKey();
...

    }
    public DbSet<T_予約> T_予約 { get; set; }
    public DbSet<T_予約sub> T_予約sub { get; set; }
    public DbSet<T_顧客> T_顧客 { get; set; }
...
}

対応するテーブルの作り方もコードファーストのデータベース作りと同じになります。やたらに Nullable が多いのと、カラム名が日本語になっているのが元データが ACCESS のためです。元の ACCESS では VBA でガシガシと組んでいて大変そうです。

public partial class T_予約
{
    public int KID { get; set; }
    public Nullable<int> TID { get; set; }
    public Nullable<int> 予約者 { get; set; }
    public Nullable<int> TaID { get; set; }
    public Nullable<System.DateTime> 日付 { get; set; }
    public Nullable<System.DateTime> 記録日 { get; set; }
    public Nullable<System.DateTime> 契約日 { get; set; }
    public Nullable<int> 予約 { get; set; }
...
}

ACCESS のカラム名は結構自由に作れるので、郵便番号の記号「〒」も使えます。ただし、C# の場合は「〒」をプロパティ名としては使えないので Column 属性で別名をあてておきます。

public partial class T_顧客Sub
{
    public int TIDa { get; set; }
    public Nullable<int> TID { get; set; }
    public string 部署名 { get; set; }
    public string TEL { get; set; }
    public string FAX { get; set; }
    public string 携帯 { get; set; }
    [Column("〒")]
    public string ZIP { get; set; }
    public string 住所1 { get; set; }
    public string 住所2 { get; set; }
...
}

ここまでできあがると、通常の EF Core と同じように ACCESS にアクセスができます。あまり無茶をすると ACCESS ファイルが壊れそうな気がするのですが、まあ SELECT だけならば大丈夫でしょう。

webapi のコントローラーで使う

LINQ 使って記述ができるので、非常に楽です。

/// <summary>
/// 日付を指定して取得する
/// </summary>
/// <param name="date"></param>
/// <returns></returns>
[HttpGet("date/{date}")]
public IEnumerable<T_予約> Get(string date)
{
    DateTime dt = DateTime.Parse(date);
    var items =  _context.T_予約
        .Where( t => t.日付 == dt.Date )
        .ToList();
    foreach ( var item in items )
    {
        item.予約Sub = _context.T_予約sub.Where(t => t.KID == item.KID)
            .OrderBy(t => t.Sort)
            .ToList();
        item.顧客 = _context.T_顧客.FirstOrDefault(t => t.TID == item.TID);
        item.顧客Sub = _context.T_顧客Sub.FirstOrDefault(t => t.TIDa == item.TIDa);
        item.会議室Item = _context.T_ROOM.FirstOrDefault(t => t.HID == item.会議室);
    }
    return items;
}

付属するデータは、LINQ の場合は Include を使って取ってきてもよいのですが、外部キーの記述が結構面倒(標準にあっていないとうまくいかないことが多い)ので、あとから手作業でとってきています。多少検索スピードは落ちますが、ツール的にはこれで十分でしょう。まじめに作るときは SQL Server 等にデータを移行&カラム名を連携しやすいように直します。

ACCESSファイルのロック

ACCESSの場合、テーブル単位ロックがかかっています。例えば、上記の場合、ACCESS 本体で「予約」テーブルを開いていると(デザインを開くも含む)と、EF Core 側で「予約」テーブルを開くことができなくてエラーになります。実際には、ロック解除待ちのタイムアウトになります。

なので、同時利用をしていると変なことになりそうなのですが、そこは運用次第ということで。ひとまず、ACCESS から SQL Server への移行ツールを作るときに EntityFrameworkCore.Jet が便利です。

カテゴリー: 開発 | ACCESS を .NET Core の EF Coreでアクセスする はコメントを受け付けていません

.NET CoreでNULL付きのデータをSQL Serverに大量投入する

2年ほど前、LINQ を使って大量データを INSERT すると遅すぎるので、LINQ の INSERT を SqlBulkCopy にするとどれだけ早くなるのか? | Moonmile Solutions Blog で、SqlBulkCopy を試しました。この記事の例では、6秒から 0.06 秒になって 100 倍になっています。実際は、仕事で使うデータ移行分があって、2万件ほどが1時間ぐらいかかっていたのが、数秒で終わるように効率化されています。

このツール自体は .NET Framework の EF6 で書いているのですが、現在 .NET5 の EF Core で書き換え中です。いままで、EF6 を使って Visual Studio 上で Model クラスを作っていたのですが、EF Core のほうでコードファースト的にテテーブル用に対応するクラスを作っています。コードファーストとはいえ、実際にデータベースに反映してはいません。しかし、いちいち Visual Studio 上でデータベース内のテーブルとの同期をとらなくてよいので楽です。

まあ、それはそれで、手作業でテーブルを書き換えてはマイグレーションするわけですが。

さて、EF Core の INSERT も EF6 の INSERT と同じようにかなり遅いです。EF6 の場合は、

  • AutoDetectChangesEnabled
  • ValidateOnSaveEnabled

を false にすることで、INSERT の高速化がそこそこできる(5倍ぐらい早くなる)のですが、数万件のデータを投入しようとする結構かかります。さらに、手元のデータでは EF6 よりも EF Core の INSERT が 5倍ぐらい遅いので、ちょっと大きめのデータだと INSERT だけでは実用的に無理ということになります。

そこで、再び SqlBulkCopy の出番なのです。以前書いていた拡張メソッド AsDataTable だと null 許容型が通らないので少し書き換えます。

public static class DataTableExtenstions
{
    public static DataTable AsDataTable<T>(this DbSet<T> src) where T : class
    {
        return DataTableExtenstions.AsDataTable(src.Local);
    }
    public static DataTable AsDataTable<T>(this IEnumerable<T> src) where T : class
    {
        var properties = typeof(T).GetProperties();
        var dest = new DataTable();
        // テーブルレイアウトの作成
        foreach (var prop in properties)
        {
            DataColumn dc = new DataColumn();
            dc.ColumnName = prop.Name;
            if (prop.PropertyType.IsGenericType && 
                prop.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                dc.DataType = Nullable.GetUnderlyingType(prop.PropertyType);
                dc.AllowDBNull = true;
            } else
            {
                dc.DataType = prop.PropertyType;
            }
            dest.Columns.Add(dc);
        }
        // 値の投げ込み
        foreach (var item in src)
        {
            var row = dest.NewRow();
            foreach (var prop in properties)
            {
                var itemValue = prop.GetValue(item, new object[] { });
                row[prop.Name] = itemValue ?? System.DBNull.Value;
            }
            dest.Rows.Add(row);
        }
        return dest;
    }
}

データを投入するときに null から System.DBNull.Value にしておきます。

private void blukSave<T>(string tablename, IEnumerable<T> items, bool keepid = true ) where T : class
{
    var cnstr = toEnt.Database.GetDbConnection().ConnectionString;
    SqlBulkCopy bc;
    if (keepid == true)
    {
        bc = new SqlBulkCopy(cnstr, SqlBulkCopyOptions.KeepIdentity);
    }
    else
    {
        bc = new SqlBulkCopy(cnstr);
    }
    bc.DestinationTableName = tablename;
    var dt = items.AsDataTable();
    bc.WriteToServer(dt);
}

確か、以前の BulkCopy は ID をインクリメントしなかったような気がするのですが、現在の SqlBulkCopy は ID を挿入時にインクリメントしてしまいます。大量データを投入するときは、ID はあらかじめ振ってあることが多い(他のデータから移行するため)ので、INSERT 時に ID の値が変わらないようにします。

オプションで SqlBulkCopyOptions.KeepIdentity をつけておきます。

実際の使い方はこんな感じ。予約テーブルは実は ACCESS から移行するデータなので大量に Nullable が入っています。いったん List にため込んでから、BlukCopy を行うので一時的にため込まれる List のメモリ量が心配ですが、まあ、大丈夫でしょう。最初の DropTable 関数は、内部で TRUNCATE TABLE を呼び出しています。

public bool To予約()
{
    DropTable("予約");
    var lst = new List<予約>();
    foreach (var it in fromEnt.T_予約)
    {
        var t = new 予約();
        t.ID = it.KID;
        t.顧客ID = it.TID.Value;
        t.顧客SUBID = it.TIDa ?? 0;
        t.予約者 = it.予約者;
        ...
        t.UpdateAt = DateTime.Now;

        lst.Add(t);
    }
    this.blukSave("予約", lst);
    return true;
}

この状態で、2万件のデータを投入すると数秒で終わります。EF Core の LINQ の INSERT を使うと、2時間ぐらいかかるので、これは実用的とは言えません。何が遅いのがいまいち不明ですね。。。

余談ですが、.NET Core の DbContext を使ったときに、AutoDetectChangesEnabled や ValidateOnSaveEnabled がありません。代わりに、OnConfiguring をオーバーライドして UseQueryTrackingBehavior を使います。トラッキングをしないようにすると、多少は早くなる(2,3倍ぐらい)のですが、SqlBulkCopy の 100倍には遠く及びません。

public class KaigiDbContext : DbContext
{
    public KaigiDbContext()
    {

    }
    public KaigiDbContext(DbContextOptions<AccessDbContext> options) : base(options)
    {
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        string cnstr = "data source=.;initial catalog=会議室;integrated security=True";
        optionsBuilder.UseSqlServer(cnstr);
        optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
    }
    public DbSet<予約> 予約 { get; set; }
...
}

カテゴリー: 開発 | .NET CoreでNULL付きのデータをSQL Serverに大量投入する はコメントを受け付けていません