さまざまなコンポーネントとドライバーを使用したFirebirdデータベースアプリケーションの作成:ADO.NET Entity Framework 6

この記事では、Entity FrameworkアクセスコンポーネントとVisual Studio 2015を使用してFirebird DBMSのアプリケーションを作成するプロセスについて説明します。



ADO.NET Entity Framework(EF) -オブジェクト指向データアクセステクノロジーは、Microsoft .NET Frameworkのオブジェクトリレーショナルマッピング(ORM)ソリューションです。 LINQ to Entitiesの形式のLINQとEntity SQLの両方を使用してオブジェクトと対話する機能を提供します。



Entity Frameworkは、データベースと対話するための3つの可能な方法を提供します。





このアプリケーションでは、コードファーストアプローチを使用しますが、他のアプローチも簡単に使用できます。



このアプリケーションは、データベースを使用して動作します。データベースのモデルを以下の図に示します。











注意!



このモデルは単なる例です。 サブジェクト領域はより複雑な場合もあれば、まったく異なる場合もあります。 この記事で使用するモデルは、コンポーネントの操作の説明、データモデルの作成と変更の説明が煩雑にならないように、可能な限り簡素化されています。





Firebirdで動作するようにVisual Studio 2015を準備する



Firebirdを使用するには、以下をインストールする必要があります。





最初の2つをインストールしても問題はありません。 現在、NuGetを使用してプロジェクトに配布およびインストールされています。 しかし、Visual Studioのウィザード用に設計された最後のライブラリは、インストールがそれほど簡単ではなく、多くの時間とエネルギーを費やす可能性があります。



親切な人々は、インストールプロセスを自動化し、すべてのコンポーネントのインストールを1つのディストリビューションに含めようとしました。 ただし、場合によっては、すべてのコンポーネントを手動でインストールする必要があります。 この場合、ダウンロードする必要があります:





以下にインストールプロセスを示します。



  1. FirebirdSql.Data.FirebirdClient-4.10.0.0.msiをインストールします
  2. EntityFramework.Firebird-4.10.0.0-NET45.7zをFirebirdクライアントがインストールされているフォルダーに解凍します。 私はこのフォルダを持っていますc:\ Program Files(x86)\ FirebirdClient \

    重要!



    これは管理者権限で実行する必要があります。 保護されたディレクトリを持つ他のアクションと同様。



  3. GACにFirebirdアセンブリをインストールする必要があります。 便宜上、.NET Framework 4.5のgacutilユーティリティへのパスを%PATH%に記述します。 このパスはc:\ Program Files(x86)\ Microsoft SDKs \ Windows \ v10.0A \ bin \ NETFX 4.6.1 Tools \
  4. 管理者としてcmdコマンドラインを起動し、クライアントがインストールされているディレクトリに移動します。

    chdir "c:\Program Files (x86)\FirebirdClient"







  5. FirebirdSql.Data.FirebirdClientがGACにインストールされていることを確認してください。 これを行うには、次のコマンドを入力します

    gacutil /l FirebirdSql.Data.FirebirdClient

    Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.0

    c (Microsoft Corporation). .



    :

    FirebirdSql.Data.FirebirdClient, Version=4.10.0.0, Culture=neutral, PublicKeyToken=3750abcc3150b00c, processorArchitecture=MSIL



    = 1







    FirebirdSql.Data.FirebirdClientがGACにインストールされていない場合は、コマンドを使用してこれを実行します

    gacutil /i FirebirdSql.Data.FirebirdClient.dll







  6. EntityFramework.FirebirdをGACに設定します

    gacutil /i EntityFramework.Firebird.dll







  7. DDEXProvider-3.0.2.0.7zを便利なディレクトリに解凍します。 c:\ Program Files(x86)\ FirebirdDDEX \で解凍しました
  8. そこで、アーカイブサブディレクトリ/ reg_files / VS2015の内容をDDEXProvider-3.0.2.0-src.7zで解凍します。

    著者メモ



    おもしろいですが、何らかの理由で、これらのファイルはコンパイル済みのdllライブラリを含む以前のアーカイブにはありませんが、ソースコードと共にアーカイブに存在します。



  9. メモ帳を使用してFirebirdDDEXProvider64.regファイルを開きます。 %path%を含む行を見つけて、FirebirdSql.VisualStudio.DataTools.dllファイルへのフルパスに変更します



    "CodeBase" = "c:\\プログラムファイル(x86)\\ FirebirdDDEX \\ FirebirdSql.VisualStudio.DataTools.dll"

  10. このファイルを保存して実行します。 レジストリに情報を追加する要求で、[はい]をクリックします。
  11. 今、machine.configファイルを編集する必要があります。私の場合は次のパスにあります:C:\ Windows \ Microsoft.NET \ Framework \ v4.0.30319 \ Config

    このファイルをメモ帳で開きます。 セクションを見つける

    <system.data>

    <DbProviderFactories>







    このセクションに行を追加します。

    <add name="FirebirdClient Data Provider" invariant="FirebirdSql.Data.FirebirdClient" description=".Net Framework Data Provider for Firebird" type="FirebirdSql.Data.FirebirdClient.FirebirdClientFactory, FirebirdSql.Data.FirebirdClient, Version=4.10.0.0, Culture=neutral, PublicKeyToken=3750abcc3150b00c" />







    発言

    これはすべてバージョン4.10.0で有効です。



    machine.configについても同じことを行います。 Cにあります:\ Windows \ Microsoft.NET \ Framework64 \ v4.0.30319 \ Config \



インストールが完了しました。



すべてが正常にインストールされたことを確認するには、Visual Studio 2015を実行します。サーバーブラウザーを見つけ、既存のFirebirdデータベースの1つに接続を試みます。



































プロジェクト作成





この記事では、Windowsフォームアプリケーションの作成例を見ていきます。 残りの種類のアプリケーションは異なりますが、Entity Frameworkを介してFirebirdを操作する原則は同じままです。



まず、Windows Formsプロジェクトを作成した後、NuGetパッケージマネージャーを使用して次のパッケージを追加する必要があります。





これを行うには、ソリューションエクスプローラーでプロジェクト名を右クリックし、ドロップダウンメニューで[NuGetパッケージの管理]を選択します。











表示されるパッケージマネージャーで、必要なパッケージを検索してインストールします。











EDMモデルの作成



このアプリケーションでは、コードファーストアプローチを使用します。



EDMモデルを作成するには、ソリューションエクスプローラーでプロジェクト名を右クリックし、メニュー項目[追加]-> [要素の作成]を選択します。











次に、新しいアイテムの追加ウィザードで、「ADO.NET EDMモデル」を選択します。











すでにデータベースがあるため( さまざまなコンポーネントとドライバーを使用したFirebird DBMSのアプリケーションの作成:FireDacを参照 )、データベースからEDMモデルを生成します。











次に、モデルを作成する接続を選択する必要があります。 そのような接続がない場合は、作成する必要があります。



















基本的な接続パラメーターに加えて、トランザクション分離レベル(デフォルトではコミットの読み取り)、接続プールの使用状況など、いくつかの追加パラメーターを指定する必要があります。











その過程で、モデル作成ウィザードは接続文字列の保存方法を尋ねます。











すべてのユーザーが同じアカウントでデータベースを操作するWebアプリケーションまたは3リンクを構築する場合は、「はい」を選択してください。 アプリケーションがデータベースに接続するための資格情報を要求する必要がある場合は、「いいえ」を選択します。 ただし、[はい]を選択すると、マスターを操作する方がはるかに便利です。 アプリケーション構成ファイル.exe.confの接続文字列を編集するだけで、完成したアプリケーションでこれをいつでも変更できます。 接続文字列は、ほぼこの形式でconnectionStringsセクションに保存されます。



 <add name="DbModel" connectionString="character set=UTF8; data source=localhost;initial catalog=examples; port number=3050; user id=sysdba; dialect=3; isolationlevel=Snapshot; pooling=True; password=masterkey;" providerName="FirebirdSql.Data.FirebirdClient" />
      
      







構成ファイルが機密情報の保存を停止するには、単純に接続文字列から削除します

 password=masterkey;
      
      







Firebird 3.0での作業に関する注意



残念ながら、Firebird(バージョン4.10.0)用の現在のADO .Netプロバイダーは、SRP認証をサポートしていません(Firebird 3.0のデフォルト)。 したがって、Firebird 3.0を使用する場合は、FirebirdがLegacy_Authを介して機能するように、firebird.conf(または特定のデータベースのdatabases.conf)の設定を変更する必要があります。 これを行うには、次の設定を変更します。

UserManager = Legacy_UserManager

WireCrypt = Disabled

AuthServer = Legacy_Auth, Srp, WinSspi







設定を保存します。 次に、Legacy_UserManagerを使用してSYSDBAユーザーと他のユーザーを作成する必要があります。





次に、モデルに含めるテーブルとビューを尋ねられます。











原則として、EDMモデルは準備ができています。 このウィザードが機能すると、5つの新しいファイルが作成されます。 1つのモデルファイルと、各モデルエンティティを説明する4つのファイル。



INVOICEの本質を説明する生成ファイルの1つを見てみましょう。



 [Table("Firebird.INVOICE")] public partial class INVOICE { [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")] public INVOICE() { INVOICE_LINES = new HashSet<INVOICE_LINE>(); } [Key] [DatabaseGenerated(DatabaseGeneratedOption.None)] public int INVOICE_ID { get; set; } public int CUSTOMER_ID { get; set; } public DateTime? INVOICE_DATE { get; set; } public double? TOTAL_SALE { get; set; } public short PAID { get; set; } public virtual CUSTOMER CUSTOMER { get; set; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] public virtual ICollection<INVOICE_LINE> INVOICE_LINES { get; set; } }
      
      







このクラスには、INVOICEテーブルのフィールドを表示するプロパティが含まれています。 これらの各プロパティには、制限を説明する属性があります。 Microsoft Code First Data Annotationsのドキュメントでさまざまな属性について詳しく読むことができます。



さらに、さらに2つのナビゲーションプロパティCUSTOMERとINVOICE_LINESが生成されました。 最初の要素にはプロバイダーエンティティへの参照が含まれ、2番目の要素には請求書の行のコレクションが含まれます。 INVOICE_LINEテーブルのINVOICEテーブルに外部キーがあるため、生成されました。 もちろん、このプロパティをINVOICEエンティティから削除できますが、これはまったく必要ありません。 実際、この場合、CUSTOMERプロパティとINVOICE_LINESプロパティはいわゆる「遅延読み込み」を使用します。 この場合、ロードはオブジェクトへの最初のアクセス時に実行されます。 関連付けられたデータが必要ない場合、ロードされません。 ただし、ナビゲーションプロパティへの最初のアクセスで、このデータはデータベースから自動的にロードされます。



遅延読み込みを使用する場合、クラスを宣言するときに留意すべき点がいくつかあります。 したがって、遅延読み込みを使用するクラスはパブリックである必要があり、そのプロパティにはパブリック修飾子と仮想修飾子が必要です。



最初の不快な驚きは、同じクラスで私たちを待っています。 TOTAL_SALEフィールドは、データベースではNUMERICタイプ(15、2)ですが、本質的にはdoubleとして表示されていたため、精度が失われています。 私はこれをFirebird ADO.NETプロバイダーのバグと見なす傾向があります。 この迷惑な見落としを修正してみましょう。 C#には、固定精度の数値に対する演算用の10進数のタイプがあります。



 public decimal TOTAL_SALE { get; set; }
      
      







さらに、FirebirdタイプNUMERIC(x、y)が使用されるすべてのエンティティのすべてのフィールドの説明を変更します。 つまり、PRODUCT.PRICE、INVOICE_LINE.QUANTITY、INVOICE_LINE.SALE_PRICE。



次に、モデル全体を記述したDbModel.csファイルを開きます。



 public partial class DbModel : DbContext { public DbModel() : base("name=DbModel") { } public virtual DbSet<CUSTOMER> CUSTOMERS { get; set; } public virtual DbSet<INVOICE> INVOICES { get; set; } public virtual DbSet<INVOICE_LINE> INVOICE_LINES { get; set; } public virtual DbSet<PRODUCT> PRODUCTS { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<CUSTOMER>() .Property(e => e.ZIPCODE) .IsFixedLength(); modelBuilder.Entity<CUSTOMER>() .HasMany(e => e.INVOICES) .WithRequired(e => e.CUSTOMER) .WillCascadeOnDelete(false); modelBuilder.Entity<PRODUCT>() .HasMany(e => e.INVOICE_LINES) .WithRequired(e => e.PRODUCT) .WillCascadeOnDelete(false); modelBuilder.Entity<INVOICE>() .HasMany(e => e.INVOICE_LINES) .WithRequired(e => e.INVOICE) .WillCascadeOnDelete(false); } }
      
      







ここでは、各エンティティのデータセットを説明するプロパティが表示されます。 Fluent APIを使用してモデルを作成するための追加のプロパティを設定します。 Fluent APIドキュメントのMicrosoft 構成/マッピングのプロパティとタイプでFluent APIの完全な説明を読むことができます。



OnModelCreatingメソッドで、Fluent APIを使用して、decimal型のプロパティの精度を設定します。 これを行うには、次の行を追加します



  modelBuilder.Entity<PRODUCT>() .Property(p => p.PRICE) .HasPrecision(15, 2); modelBuilder.Entity<INVOICE>() .Property(p => p.TOTAL_SALE) .HasPrecision(15, 2); modelBuilder.Entity<INVOICE_LINE>() .Property(p => p.SALE_PRICE) .HasPrecision(15, 2); modelBuilder.Entity<INVOICE_LINE>() .Property(p => p.QUANTITY) .HasPrecision(15, 0);
      
      







ユーザーインターフェイスの作成



このアプリケーションでは、商品のディレクトリと顧客のディレクトリの2つのディレクトリを作成します。 各ディレクトリには、DataGridViewグリッド、ToolStripボタンのあるパネル、およびフォーム上のコントロールへのデータバインディングを簡素化するために使用されるBindingSourceコンポーネントが含まれています。











両方のディレクトリの機能は類似しており、同様の方法で実装されているため、1つだけを説明します。



コンテキストを取得する



モデルを操作するには、コンテキスト(またはモデル)を取得するメソッドが必要です。 原則として、これで十分です。



 DbModel dbContext = new DbModel();
      
      







ただし、機密データ(パスワードなど)が接続文字列に保存されておらず、アプリケーションの起動時に認証中に機密データを初期化する場合、接続文字列の保存と復元、または以前に作成されたコンテキストの保存に特別な方法が必要になります。 これを行うには、コンテキストを取得するためのメソッドに加えて、アプリケーションレベル(作業期間など)でいくつかのグローバル変数を含む特別なクラスを作成します。



 static class AppVariables { private static DbModel dbContext = null; /// <summary> ///     /// </summary> public static DateTime StartDate { get; set; } /// <summary> ///     /// </summary> public static DateTime FinishDate { get; set; } /// <summary> ///    () /// </summary> /// <returns></returns> public static DbModel CreateDbContext() { dbContext = dbContext ?? new DbModel(); return dbContext; } }
      
      







接続文字列自体は、認証が正常に渡された後、アプリケーションの起動時に初期化されます。 これを行うには、メインフォームのLoadイベントハンドラーで、次のコードを記述します。



 private void MainForm_Load(object sender, EventArgs e) { var dialog = new LoginForm(); if (dialog.ShowDialog() == DialogResult.OK) { var dbContext = AppVariables.getDbContext(); try { string s = dbContext.Database.Connection.ConnectionString; var builder = new FbConnectionStringBuilder(s); builder.UserID = dialog.UserName; builder.Password = dialog.Password; dbContext.Database.Connection.ConnectionString = builder.ConnectionString; //   dbContext.Database.Connection.Open(); } catch (Exception ex) { //   MessageBox.Show(ex.Message, "Error"); Application.Exit(); } } else Application.Exit(); }
      
      







次に、コンテキストを取得するために、静的なCreateDbContextメソッドを使用します。

 var dbContext = AppVariables.getDbContext()
      
      







データを操作する



モデルエンティティ自体にはデータが含まれていません。 データをロードする最も簡単な方法は、たとえば次のようなLoadメソッドを呼び出すことです。



 private void LoadCustomersData() { dbContext.CUSTOMERS.Load(); var customers = dbContext.CUSTOMERS.Local; bindingSource.DataSource = customers.ToBindingList(); } private void CustomerForm_Load(object sender, EventArgs e) { LoadCustomersData(); dataGridView.DataSource = bindingSource; dataGridView.Columns["CUSTOMER_ID"].Visible = false; }
      
      







ただし、この方法にはいくつかの欠点があります。

  1. Loadメソッドは、CUSTOMERテーブルからすべてのデータをすぐにメモリにロードします。
  2. 遅延プロパティ(INVOICES)は、すぐには読み込まれませんが、アクセスしたときにのみ読み込まれますが、グリッドにレコードを表示するときにも読み込まれます。 また、レコードの数とまったく同じ回数が表示されます。
  3. エントリの順序は未定義です。


これらの欠点を回避するために、LINQ(Language Integrated Query)テクノロジ、またはむしろLINQ to Entitiesを使用します。 LINQ to Entitiesは、形式がSQL式に似ている式を使用してデータを取得するためのシンプルで直感的なアプローチを提供します。 LINQ to EntitiesLINQ構文について学ぶことができます。



LINQ拡張メソッドは、 IEnumerableIQueryableの 2つのオブジェクトを返すことができます。 IQueryableインターフェイスはIEnumerableを継承するため、理論上、IQueryableオブジェクトはIEnumerableオブジェクトでもあります。 しかし、それらの間には大きな違いがあります。



IEnumerableインターフェイスはSystem.Collections名前空間にあります。 IEnumerableオブジェクトはメモリ内のデータセットを表し、そのデータに沿ってのみ前方に移動できます。 リクエストを実行すると、IEnumerableはすべてのデータをロードし、それをフィルタリングする必要がある場合、フィルタリング自体がクライアント側で発生します。



IQueryableインターフェイスはSystem.Linq名前空間にあります。 IQueryableオブジェクトは、データベースへのリモートアクセスを提供し、最初から最後までの直接の順序と逆の順序の両方でデータをナビゲートできるようにします。 戻りオブジェクトがIQueryableであるクエリを作成するプロセスで、クエリが最適化されます。 その結果、実行プロセスで消費されるメモリが減り、ネットワーク帯域幅が減ります。



Localプロパティは、IEnumerableインターフェイスを返します。 したがって、LINQクエリを作成できます。



 private void LoadCustomersData() { var dbContext = AppVariables.getDbContext(); dbContext.CUSTOMERS.Load(); var customers = from customer in dbContext.CUSTOMERS.Local orderby customer.NAME select new customer; bindingSource.DataSource = customers.ToBindingList(); }
      
      







ただし、既に述べたように、この要求はメモリ内のデータに対して実行されます。 原則として、事前フィルタリングを必要としない小さなテーブルの場合、これは許容されます。



LINQクエリをSQLに変換してサーバー側で実行するには、dbContext.CUSTOMERS.LocalのdbContext.CUSTOMERS.Localプロパティを使用してdbContext.CUSTOMERSにすぐにアクセスする必要があります。 この場合、dbContext.CUSTOMERS.Load()への予備的な呼び出しは不要です。

コレクションをメモリにロードします。



しかし、ここでは小さな待ち伏せを待っています。 IQueryableオブジェクトはBindingListを返すことはできません。 BindingListは、双方向のデータバインディングメカニズムを作成するための基本クラスです。 IQueryableインターフェイスからToListを呼び出すことで通常のリストを取得できますが、この場合、グリッドやその他の多くでの並べ替えなどの楽しいボーナスを失います。 ところで、.NET Framework 5は既にこれを修正し、特別な拡張機能を作成しています。 同じことをする拡張機能を作成しましょう。



 public static class DbExtensions { //         private class IdResult { public int Id { get; set; } } //  IQueryable  BindingList public static BindingList<T> ToBindingList<T> (this IQueryable<T> source) where T : class { return (new ObservableCollection<T>(source)).ToBindingList(); } //     public static int NextValueFor(this DbModel dbContext, string genName) { string sql = String.Format( "SELECT NEXT VALUE FOR {0} AS Id FROM RDB$DATABASE", genName); return dbContext.Database.SqlQuery<IdResult>(sql).First().Id; } //     DbSet   //     public static void DetachAll<T>(this DbModel dbContext, DbSet<T> dbSet) where T : class { foreach (var obj in dbSet.Local.ToList()) { dbContext.Entry(obj).State = EntityState.Detached; } } //       public static void Refresh(this DbModel dbContext, RefreshMode mode, IEnumerable collection) { var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; objectContext.Refresh(mode, collection); } //   public static void Refresh(this DbModel dbContext, RefreshMode mode, object entity) { var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; objectContext.Refresh(mode, entity); } }
      
      







同じクラスには、さらにいくつかの拡張機能があります。



NextValueForメソッドは、次のジェネレーター値を取得することを目的としています。 dbContext.Database.SqlQueryメソッドを使用すると、SQLクエリを直接実行し、その結果をエンティティ(投影)にマッピングできます。 SQLクエリを直接実行する必要がある場合に使用できます。



DetachAllメソッドは、DBSetコレクション内のすべてのオブジェクトをコンテキストから切り離すように設計されています。 これは、内部キャッシュを更新するために必要です。 実際には、コンテキストのコンテキストでは、取得されたすべてがキャッシュされ、データベースから再度取得されることはありません。 ただし、異なるコンテキストで作成された変更されたレコードを取得するのが難しくなるため、これは必ずしも有用ではありません。



発言



Webアプリケーションでは、コンテキストは通常​​非常に短い期間しか存続せず、新しいコンテキストには空のキャッシュがあります。





Refreshメソッドは、エンティティオブジェクトのプロパティを更新するように設計されています。 編集または追加後にオブジェクトのプロパティを更新するのに役立ちます。



したがって、データ読み込みコードは次のようになります



  private void LoadCustomersData() { var dbContext = AppVariables.getDbContext(); //     //       //        dbContext.DetachAll(dbContext.CUSTOMERS); var customers = from customer in dbContext.CUSTOMERS orderby customer.NAME select customer; bindingSource.DataSource = customers.ToBindingList(); } private void CustomerForm_Load(object sender, EventArgs e) { LoadCustomersData(); dataGridView.DataSource = bindingSource; dataGridView.Columns["INVOICES"].Visible = false; dataGridView.Columns["CUSTOMER_ID"].Visible = false; dataGridView.Columns["NAME"].HeaderText = "Name"; dataGridView.Columns["ADDRESS"].HeaderText = "Address"; dataGridView.Columns["ZIPCODE"].HeaderText = "ZipCode"; dataGridView.Columns["PHONE"].HeaderText = "Phone"; }
      
      







追加ボタンをクリックしたときのイベントハンドラーのコードは次のとおりです。



 private void btnAdd_Click(object sender, EventArgs e) { var dbContext = AppVariables.getDbContext(); //     var customer = (CUSTOMER)bindingSource.AddNew(); //     using (CustomerEditorForm editor = new CustomerEditorForm()) { editor.Text = " "; editor.Customer = customer; //    editor.FormClosing += delegate (object fSender, FormClosingEventArgs fe) { if (editor.DialogResult == DialogResult.OK) { try { //     //     customer.CUSTOMER_ID = dbContext.NextValueFor("GEN_CUSTOMER_ID"); //    dbContext.CUSTOMERS.Add(customer); //    dbContext.SaveChanges(); //     dbContext.Refresh(RefreshMode.StoreWins, customer); } catch (Exception ex) { //   MessageBox.Show(ex.Message, "Error"); //        fe.Cancel = true; } } else bindingSource.CancelEdit(); }; //    editor.ShowDialog(this); } }
      
      







新しいレコードを追加するとき、ジェネレーターを使用して次の識別子の値を取得します。 識別子の値を初期化できませんでした。この場合、BEFORE INSERTトリガーは機能しますが、それでも次のジェネレーター値をプルします。 ただし、この場合、新しく追加されたレコードを更新することはできません。



編集ボタンをクリックしたときのイベントハンドラーのコードは次のとおりです。



 private void btnEdit_Click(object sender, EventArgs e) { var dbContext = AppVariables.getDbContext(); //   var customer = (CUSTOMER)bindingSource.Current; //     using (CustomerEditorForm editor = new CustomerEditorForm()) { editor.Text = " "; editor.Customer = customer; //    editor.FormClosing += delegate (object fSender, FormClosingEventArgs fe) { if (editor.DialogResult == DialogResult.OK) { try { //    dbContext.SaveChanges(); dbContext.Refresh(RefreshMode.StoreWins, customer); //     bindingSource.ResetCurrentItem(); } catch (Exception ex) { //   MessageBox.Show(ex.Message, "Error"); //        fe.Cancel = true; } } else bindingSource.CancelEdit(); }; //    editor.ShowDialog(this); } }
      
      







顧客を編集するためのフォームは次のとおりです。











データバインディングコードは非常に単純です。



 public CUSTOMER Customer { get; set; } private void CustomerEditorForm_Load(object sender, EventArgs e) { edtName.DataBindings.Add("Text", this.Customer, "NAME"); edtAddress.DataBindings.Add("Text", this.Customer, "ADDRESS"); edtZipCode.DataBindings.Add("Text", this.Customer, "ZIPCODE"); edtPhone.DataBindings.Add("Text", this.Customer, "PHONE"); }
      
      







削除ボタンをクリックしたときのイベントハンドラーのコードは次のとおりです。



 private void btnDelete_Click(object sender, EventArgs e) { var dbContext = AppVariables.getDbContext(); var result = MessageBox.Show("    ?", "", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { //   var customer = (CUSTOMER)bindingSource.Current; try { dbContext.CUSTOMERS.Remove(customer); //    dbContext.SaveChanges(); //     bindingSource.RemoveCurrent(); } catch (Exception ex) { //   MessageBox.Show(ex.Message, "Error"); } } }
      
      







雑誌



このアプリケーションでは、1つのジャーナル「請求書」があります。ディレクトリとは異なり、ジャーナルにはかなり多数のエントリが含まれており、多くの場合更新されます。



請求書-一般属性(番号、日付、顧客...)を説明する見出しと、商品、数量、コストなどのリストを含む請求書行で構成されます。このようなドキュメントの場合、2つのグリッドを使用すると便利です。メインには、ドキュメントヘッダーのデータが表示され、詳細には、商品のリストが表示されます。したがって、ドキュメントのフォームに、それぞれが独自のBindingSourceをバインドする2つのDataGridViewコンポーネントを配置する必要があります。











ほとんどのログには、ドキュメントが作成された日付のフィールドが含まれています。選択されるデータの量を減らすために、通常、クライアントに送信されるデータの量を減らすために、作業期間などの概念を導入するのが慣習です。作業期間は、作業文書が必要な日付範囲です。アプリケーションには複数のログを含めることができるため、作業期間の開始日と終了日を含む変数をグローバルAppVariablesモジュール(コンテキストの取得を参照)に配置することは理にかなっています。これは何らかの方法でデータベースを操作するすべてのモジュールで使用されます。アプリケーションが開始されると、通常、作業期間は現在の四半期の開始日と終了日によって初期化されます(他のオプションがある場合があります)。アプリケーション中に、ユーザーの要求に応じて勤務期間を変更できます。



最も最近入力された文書が最も頻繁に必要とされるため、日付の逆順に並べ替えることは理にかなっています。ディレクトリの場合のようにデータを取得するには、LINQを使用します。上記に基づいて、請求書の上限のデータを読み込む方法は次のようになります。

 public void LoadInvoicesData() { var dbContext = AppVariables.getDbContext(); //   LINQ   SQL var invoices = from invoice in dbContext.INVOICES where (invoice.INVOICE_DATE >= AppVariables.StartDate) && (invoice.INVOICE_DATE <= AppVariables.FinishDate) orderby invoice.INVOICE_DATE descending select new InvoiceView { Id = invoice.INVOICE_ID, Cusomer_Id = invoice.CUSTOMER_ID, Customer = invoice.CUSTOMER.NAME, Date = invoice.INVOICE_DATE, Amount = invoice.TOTAL_SALE, Paid = (invoice.PAID == 1) ? "Yes" : "No" }; masterBinding.DataSource = invoices.ToBindingList(); }
      
      







予測として、匿名型ではなく、InvoiceViewクラスを使用しました。これにより、型キャストが簡素化されます。InvoiceViewクラスの定義は次のとおりです。

 public class InvoiceView { public int Id { get; set; } public int Cusomer_Id { get; set; } public string Customer { get; set; } public DateTime? Date { get; set; } public decimal? Amount { get; set; } public string Paid { get; set; } public void Load(int Id) { var dbContext = AppVariables.getDbContext(); var invoices = from invoice in dbContext.INVOICES where invoice.INVOICE_ID == Id select new InvoiceView { Id = invoice.INVOICE_ID, Cusomer_Id = invoice.CUSTOMER_ID, Customer = invoice.CUSTOMER.NAME, Date = invoice.INVOICE_DATE, Amount = invoice.TOTAL_SALE, Paid = (invoice.PAID == 1) ? "Yes" : "No" }; InvoiceView invoiceView = invoices.ToList().First(); this.Id = invoiceView.Id; this.Cusomer_Id = invoiceView.Cusomer_Id; this.Customer = invoiceView.Customer; this.Date = invoiceView.Date; this.Amount = invoiceView.Amount; this.Paid = invoiceView.Paid; } }
      
      







Loadメソッドを使用すると、すべてのレコードを完全に再ロードするのではなく、グリッドで追加または更新された1つのレコードをすばやく更新できます。



追加ボタンをクリックしたときのイベントハンドラーのコードは次のとおりです。

 private void btnAddInvoice_Click(object sender, EventArgs e) { var dbContext = AppVariables.getDbContext(); var invoice = dbContext.INVOICES.Create(); using (InvoiceEditorForm editor = new InvoiceEditorForm()) { editor.Text = "  "; editor.Invoice = invoice; //    editor.FormClosing += delegate (object fSender, FormClosingEventArgs fe) { if (editor.DialogResult == DialogResult.OK) { try { //    invoice.INVOICE_ID = dbContext.NextValueFor("GEN_INVOICE_ID"); //   dbContext.INVOICES.Add(invoice); //    dbContext.SaveChanges(); //       ((InvoiceView)masterBinding.AddNew()).Load(invoice.INVOICE_ID); } catch (Exception ex) { //   MessageBox.Show(ex.Message, "Error"); //        fe.Cancel = true; } } }; //    editor.ShowDialog(this); } }
      
      







同様のディレクトリメソッドとは異なり、ここでレコードはdbContext.Refreshを呼び出すのではなく、InvoiceViewプロジェクションのLoadメソッドを使用して更新されます。実際のところ、dbContext.Refreshは、複雑なLINQクエリで取得できる任意の投影ではなく、エンティティオブジェクトを更新するように設計されています。



編集ボタンをクリックしたときのイベントハンドラーのコードは次のとおりです。

 private void btnEditInvoice_Click(object sender, EventArgs e) { //   var dbContext = AppVariables.getDbContext(); //     var invoice = dbContext.INVOICES.Find(this.CurrentInvoice.Id); if (invoice.PAID == 1) { MessageBox.Show("  ,    .", ""); return; } using (InvoiceEditorForm editor = new InvoiceEditorForm()) { editor.Text = "Edit invoice"; editor.Invoice = invoice; //    editor.FormClosing += delegate (object fSender, FormClosingEventArgs fe) { if (editor.DialogResult == DialogResult.OK) { try { //    dbContext.SaveChanges(); //   CurrentInvoice.Load(invoice.INVOICE_ID); masterBinding.ResetCurrentItem(); } catch (Exception ex) { //   MessageBox.Show(ex.Message, "Error"); //        fe.Cancel = true; } } }; //    editor.ShowDialog(this); } }
      
      







ここでは、現在のレコードで利用可能な識別子によってエンティティを見つける必要がありました。CurrentInvoiceプロパティは、グリッドで選択された請求書を受け取るためのものです。次のように実装されます。

 public InvoiceView CurrentInvoice { get { return (InvoiceView)masterBinding.Current; } }
      
      







ヘッダー請求書を削除すると、自分で行うことができます。



請求書の追加、編集、削除に加えて、別の支払い操作を導入しました。この操作を実装するメソッドのコードは次のとおりです。

 private void btnInvoicePay_Click(object sender, EventArgs e) { var dbContext = AppVariables.getDbContext(); var invoice = dbContext.INVOICES.Find(this.CurrentInvoice.Id); try { if (invoice.PAID == 1) throw new Exception("  ,    ."); invoice.PAID = 1; //   dbContext.SaveChanges(); //    CurrentInvoice.Load(invoice.INVOICE_ID); masterBinding.ResetCurrentItem(); } catch (Exception ex) { //   MessageBox.Show(ex.Message, ""); } }
      
      







請求書アイテムを表示するには、2つの方法があります。

  1. ナビゲーションプロパティINVOICE_LINEから各請求書のデータを受け取り、この複雑なプロパティの内容を(おそらくLINQ変換で)詳細グリッドに表示します。
  2. マスターグリッドのポインターに移動すると超過する、個別のLINQ要求で各請求書のデータを受信します。




それぞれの方法には長所と短所があります。



最初の方法では、請求書フォームを開いたときに、指定された期間のすべての請求書とその位置の関連データをすぐに抽出する必要があると想定しています。これは単一のSQLクエリによって実行されますが、多くの時間がかかり、大量のRAMを必要とします。この方法は、レコードの出力が通常ページごとに発生するWEBアプリケーションにより適しています。



2番目の方法は実装がやや難しくなりますが、請求書フォームをすばやく開くことができ、リソースへの要求が少なくなりますが、ポインターをマスターグリッドに移動するたびに、SQLクエリがいっぱいになり、ネットワークトラフィックが読み込まれます(ただし、量は少なくなります)。



このアプリケーションでは、2番目のアプローチを使用します。これを行うには、BindingSourceコンポーネントの現在のレコード変更イベントハンドラを記述する必要があります。

 private void masterBinding_CurrentChanged(object sender, EventArgs e) { LoadInvoiceLineData(this.CurrentInvoice.Id); detailGridView.DataSource = detailBinding; }
      
      







請求書アイテムにデータをロードする方法は次のとおりです。

 private void LoadInvoiceLineData(int? id) { var dbContext = AppVariables.getDbContext(); var lines = from line in dbContext.INVOICE_LINES where line.INVOICE_ID == id select new InvoiceLineView { Id = line.INVOICE_LINE_ID, Invoice_Id = line.INVOICE_ID, Product_Id = line.PRODUCT_ID, Product = line.PRODUCT.NAME, Quantity = line.QUANTITY, Price = line.SALE_PRICE, Total = Math.Round(line.QUANTITY * line.SALE_PRICE, 2) }; detailBinding.DataSource = lines.ToBindingList(); }
      
      







プロジェクションとして、InvoiceLineViewクラスを使用しました。

 public class InvoiceLineView { public int Id { get; set; } public int Invoice_Id { get; set; } public int Product_Id { get; set; } public string Product { get; set; } public decimal Quantity { get; set; } public decimal Price { get; set; } public decimal Total { get; set; } }
      
      







InvoiceViewクラスとは異なり、1つの現在のレコードを読み込む方法はありません。ここでは、1つのドキュメントに何千もの位置が含まれていないため、子グリッドの再読み込みの速度はそれほど重要ではありませんが、必要に応じてこのメソッドを実装できます。



詳細グリッドで選択されたドキュメントの現在の行を取得する特別なプロパティを追加します。

 public InvoiceLineView CurrentInvoiceLine { get { return (InvoiceLineView)detailBinding.Current; } }
      
      







追加、編集、および削除の方法では、Entity Frameworkでストアドプロシージャを操作する方法を示します。たとえば、新しいレコードを追加するメソッドは次のようになります。

 private void btnAddInvoiceLine_Click(object sender, EventArgs e) { var dbContext = AppVariables.getDbContext(); //   - var invoice = dbContext.INVOICES.Find(this.CurrentInvoice.Id); //     - if (invoice.PAID == 1) { MessageBox.Show(" , - .", "Error"); return; } //   - var invoiceLine = dbContext.INVOICE_LINES.Create(); invoiceLine.INVOICE_ID = invoice.INVOICE_ID; //      using (InvoiceLineEditorForm editor = new InvoiceLineEditorForm()) { editor.Text = "Add invoice line"; editor.InvoiceLine = invoiceLine; //    editor.FormClosing += delegate (object fSender, FormClosingEventArgs fe) { if (editor.DialogResult == DialogResult.OK) { try { //    var invoiceIdParam = new FbParameter("INVOICE_ID", FbDbType.Integer); var productIdParam = new FbParameter("PRODUCT_ID", FbDbType.Integer); var quantityParam = new FbParameter("QUANTITY", FbDbType.Integer); //    invoiceIdParam.Value = invoiceLine.INVOICE_ID; productIdParam.Value = invoiceLine.PRODUCT_ID; quantityParam.Value = invoiceLine.QUANTITY; //    dbContext.Database.ExecuteSqlCommand( "EXECUTE PROCEDURE SP_ADD_INVOICE_LINE(@INVOICE_ID, @PRODUCT_ID, @QUANTITY)", invoiceIdParam, productIdParam, quantityParam); //   //    - CurrentInvoice.Load(invoice.INVOICE_ID); //      LoadInvoiceLineData(invoice.INVOICE_ID); //    masterBinding.ResetCurrentItem(); } catch (Exception ex) { //   MessageBox.Show(ex.Message, "Error"); //        fe.Cancel = true; } } }; //    editor.ShowDialog(this); } }
      
      







ここでは、フィールドの1つ(TotalSale)にドキュメントの行に関する集約情報が含まれているため、マスターグリッドのレコードを更新する必要があります。



レコードを更新するメソッドは、次のように実装されます。

 private void btnEditInvoiceLine_Click(object sender, EventArgs e) { var dbContext = AppVariables.getDbContext(); //   - var invoice = dbContext.INVOICES.Find(this.CurrentInvoice.Id); //     - if (invoice.PAID == 1) { MessageBox.Show("  ,   .", "Error"); return; } //    - var invoiceLine = invoice.INVOICE_LINES .Where(p => p.INVOICE_LINE_ID == this.CurrentInvoiceLine.Id) .First(); //      using (InvoiceLineEditorForm editor = new InvoiceLineEditorForm()) { editor.Text = "Edit invoice line"; editor.InvoiceLine = invoiceLine; //    editor.FormClosing += delegate (object fSender, FormClosingEventArgs fe) { if (editor.DialogResult == DialogResult.OK) { try { //    var idParam = new FbParameter("INVOICE_LINE_ID", FbDbType.Integer); var quantityParam = new FbParameter("QUANTITY", FbDbType.Integer); //    idParam.Value = invoiceLine.INVOICE_LINE_ID; quantityParam.Value = invoiceLine.QUANTITY; //    dbContext.Database.ExecuteSqlCommand( "EXECUTE PROCEDURE SP_EDIT_INVOICE_LINE(@INVOICE_LINE_ID, @QUANTITY)", idParam, quantityParam); //   //    - CurrentInvoice.Load(invoice.INVOICE_ID); //      LoadInvoiceLineData(invoice.INVOICE_ID); //    masterBinding.ResetCurrentItem(); } catch (Exception ex) { //   MessageBox.Show(ex.Message, "Error"); //        fe.Cancel = true; } } }; //    editor.ShowDialog(this); } }
      
      







レコードを削除する方法は、次のように実装されます。

 private void btnDeleteInvoiceLine_Click(object sender, EventArgs e) { var result = MessageBox.Show("     -?", "", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { var dbContext = AppVariables.getDbContext(); //   - var invoice = dbContext.INVOICES.Find(this.CurrentInvoice.Id); try { //     - if (invoice.PAID == 1) throw new Exception("   , - ."); //    var idParam = new FbParameter("INVOICE_LINE_ID", FbDbType.Integer); //    idParam.Value = this.CurrentInvoiceLine.Id; //    dbContext.Database.ExecuteSqlCommand( "EXECUTE PROCEDURE SP_DELETE_INVOICE_LINE(@INVOICE_LINE_ID)", idParam); //   //    - CurrentInvoice.Load(invoice.INVOICE_ID); //      LoadInvoiceLineData(invoice.INVOICE_ID); //    masterBinding.ResetCurrentItem(); } catch (Exception ex) { //   MessageBox.Show(ex.Message, "Error"); } } }
      
      







請求書アイテムを追加および編集する方法では、編集用のフォームを使用しました。











商品を表示するには、TextBoxを使用します。その横にあるボタンを押すと、グリッド付きのモーダルフォームが呼び出され、製品が選択されます。製品を選択するためのモーダルウィンドウとして、それらを表示するために作成されたのと同じフォームを使用します。ボタンクリックとフォーム初期化ハンドラーのコードは次のようになります。

 public partial class InvoiceLineEditorForm : Form { public InvoiceLineEditorForm() { InitializeComponent(); } public INVOICE_LINE InvoiceLine { get; set; } private void InvoiceLineEditorForm_Load(object sender, EventArgs e) { if (this.InvoiceLine.PRODUCT != null) { edtProduct.Text = this.InvoiceLine.PRODUCT.NAME; edtPrice.Text = this.InvoiceLine.PRODUCT.PRICE.ToString("F2"); btnChooseProduct.Click -= this.btnChooseProduct_Click; } if (this.InvoiceLine.QUANTITY == 0) this.InvoiceLine.QUANTITY = 1; edtQuantity.DataBindings.Add("Value", this.InvoiceLine, "QUANTITY"); } private void btnChooseProduct_Click(object sender, EventArgs e) { GoodsForm goodsForm = new GoodsForm(); if (goodsForm.ShowDialog() == DialogResult.OK) { InvoiceLine.PRODUCT_ID = goodsForm.CurrentProduct.Id; edtProduct.Text = goodsForm.CurrentProduct.Name; edtPrice.Text = goodsForm.CurrentProduct.Price.ToString("F2"); } } }
      
      







トランザクションを操作する



追加、更新、および削除時にSaveChanges()メソッドを呼び出すと、Entity Frameworkは実際に暗黙的に起動し、トランザクションを完了します。分離モデルが使用されるため、すべての操作は単一のトランザクション内で発生します。さらに、EFはデータが取得されるたびにトランザクションを自動的に開始および完了します。次の例で自動トランザクションの操作を検討してください。グリッドで強調表示されている商品を割引する必要があるとします。トランザクションを明示的に使用しないコードは次のようになります。

 var dbContext = AppVariables.getDbContext(); foreach (DataGridViewRow gridRows in dataGridView.SelectedRows) { int id = (int)gridRows.Cells["Id"].Value; //        var product = dbContext.PRODUCTS.Find(id); //  10% decimal discount = 10.0m; product.PRICE = product.PRICE * (100 - discount) /100; } //        //       dbContext.SaveChanges();
      
      







10個の製品を選択したとします。この場合、10件のトランザクションが暗黙的に使用され、識別子による商品の検索と11件目の変更が保存されます。この場合、明示的なトランザクション管理を使用する場合、使用できるトランザクションは1つだけです。たとえば、次のように:

 var dbContext = AppVariables.getDbContext(); //      using (var dbTransaction = dbContext.Database.BeginTransaction()) { string sql = "UPDATE PRODUCT " + "SET PRICE = PRICE * ROUND((100 - @DISCOUNT)/100, 2) " + "WHERE PRODUCT_ID = @PRODUCT_ID"; try { //    var idParam = new FbParameter("PRODUCT_ID", FbDbType.Integer); var discountParam = new FbParameter("DISCOUNT", FbDbType.Decimal); //  SQL     var sqlCommand = dbContext.Database.Connection.CreateCommand(); sqlCommand.CommandText = sql; //  ,    sqlCommand.Transaction = dbTransaction.UnderlyingTransaction; sqlCommand.Parameters.Add(discountParam); sqlCommand.Parameters.Add(idParam); //   sqlCommand.Prepare(); //       foreach (DataGridViewRow gridRows in dataGridView.SelectedRows) { int id = (int)gridRows.Cells["Id"].Value; //    idParam.Value = id; discountParam.Value = 10.0m; //  10% //  sql  sqlCommand.ExecuteNonQuery(); } dbTransaction.Commit(); } catch (Exception ex) { dbTransaction.Rollback(); MessageBox.Show(ex.Message, "error"); } }
      
      







この場合、デフォルトのパラメーターでトランザクションを開始しました。トランザクションパラメータを設定するには、UseTransactionメソッドを使用する必要があります。

 private void btnDiscount_Click(object sender, EventArgs e) { DiscountEditorForm editor = new DiscountEditorForm(); editor.Text = "Enter discount"; if (editor.ShowDialog() != DialogResult.OK) return; bool needUpdate = false; var dbContext = AppVariables.getDbContext(); var connection = dbContext.Database.Connection; //      using (var dbTransaction = connection.BeginTransaction(IsolationLevel.Snapshot)) { dbContext.Database.UseTransaction(dbTransaction); string sql = "UPDATE PRODUCT " + "SET PRICE = ROUND(PRICE * (100 - @DISCOUNT)/100, 2) " + "WHERE PRODUCT_ID = @PRODUCT_ID"; try { //    var idParam = new FbParameter("PRODUCT_ID", FbDbType.Integer); var discountParam = new FbParameter("DISCOUNT", FbDbType.Decimal); //  SQL     var sqlCommand = connection.CreateCommand(); sqlCommand.CommandText = sql; //      sqlCommand.Transaction = dbTransaction; sqlCommand.Parameters.Add(discountParam); sqlCommand.Parameters.Add(idParam); //   sqlCommand.Prepare(); //       foreach (DataGridViewRow gridRows in dataGridView.SelectedRows) { int id = (int)gridRows.Cells["PRODUCT_ID"].Value; //    idParam.Value = id; discountParam.Value = editor.Discount; //  sql  needUpdate = (sqlCommand.ExecuteNonQuery() > 0) || needUpdate; } dbTransaction.Commit(); } catch (Exception ex) { dbTransaction.Rollback(); MessageBox.Show(ex.Message, "error"); needUpdate = false; } } //   <a href="http://ib-aid.com/download/docs/NET_DB.zip"></a> if (needUpdate) { //       foreach (DataGridViewRow gridRows in dataGridView.SelectedRows) { var product = (PRODUCT)bindingSource.List[gridRows.Index]; dbContext.Refresh(RefreshMode.StoreWins, product); } bindingSource.ResetBindings(false); } }
      
      







よくここに。現在、更新セット全体に対して1つのトランザクションのみを使用しており、データを検索するための追加のコマンドはありません。グリッド内の割引値を入力してデータを更新するダイアログを追加するだけです。自分でやってみてください。



この記事が、Firebird DBMSを使用する際にEntity Frameworkを使用してC#でアプリケーションを作成する機能を理解するのに役立つことを願っています。











参照資料



サンプルアプリケーション データベースのソースコードと

その作成用スクリプト



All Articles