web-dev-qa-db-ja.com

エンティティフレームワークがテンポラルテーブルで機能しない

データベースファーストエンティティフレームワーク6を使用しています。スキーマ内の一部のテーブルを一時テーブルに変更した後、新しいデータを挿入しようとすると、次のエラーが発生し始めました。

Cannot insert an explicit value into a GENERATED ALWAYS column in table '<MyDatabase>.dbo.<MyTableName>. Use INSERT with a column list to exclude the GENERATED ALWAYS column, or insert a DEFAULT into GENERATED ALWAYS column.

EFがシステムによって管理されているPERIOD列の値を更新しようとしているようです。

EDMXファイルから列を削除すると問題が修正されるようですが、モデルがデータベースから再生成されるたびに列が再追加されるため、これは実行可能な解決策ではありません。

15
Matt Ruwe

この問題には2つの解決策があります。

  1. EDMXデザイナの列のプロパティウィンドウで、StoreGeneratedPattern列(私の場合はValidFromとValidTo)のPERIODidentityに変更します。 identityを使用した単なる挿入ではなく、計算によってEFが挿入と更新の値を更新するため、IDは計算よりも優れています。
  2. IDbCommandTreeInterceptor実装を作成して、ピリオド列を削除します。これは、モデルに新しいテーブルを追加するときに追加の作業を必要としないため、私の推奨するソリューションです。

これが私の実装です:

using System.Data.Entity.Infrastructure.Interception; 
using System.Data.Entity.Core.Common.CommandTrees; 
using System.Data.Entity.Core.Metadata.Edm; 
using System.Collections.ObjectModel;

internal class TemporalTableCommandTreeInterceptor : IDbCommandTreeInterceptor
{
    private static readonly List<string> _namesToIgnore = new List<string> { "ValidFrom", "ValidTo" };

    public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
    {
        if (interceptionContext.OriginalResult.DataSpace == DataSpace.SSpace)
        {
            var insertCommand = interceptionContext.Result as DbInsertCommandTree;
            if (insertCommand != null)
            {
                var newSetClauses = GenerateSetClauses(insertCommand.SetClauses);

                var newCommand = new DbInsertCommandTree(
                    insertCommand.MetadataWorkspace,
                    insertCommand.DataSpace,
                    insertCommand.Target,
                    newSetClauses,
                    insertCommand.Returning);

                interceptionContext.Result = newCommand;
            }

            var updateCommand = interceptionContext.Result as DbUpdateCommandTree;
            if (updateCommand != null)
            {
                var newSetClauses = GenerateSetClauses(updateCommand.SetClauses);

                var newCommand = new DbUpdateCommandTree(
                    updateCommand.MetadataWorkspace,
                    updateCommand.DataSpace,
                    updateCommand.Target,
                    updateCommand.Predicate,
                    newSetClauses,
                    updateCommand.Returning);

                interceptionContext.Result = newCommand;
            }
        }
    }

    private static ReadOnlyCollection<DbModificationClause> GenerateSetClauses(IList<DbModificationClause> modificationClauses)
    {
        var props = new List<DbModificationClause>(modificationClauses);
        props = props.Where(_ => !_namesToIgnore.Contains((((_ as DbSetClause)?.Property as DbPropertyExpression)?.Property as EdmProperty)?.Name)).ToList();

        var newSetClauses = new ReadOnlyCollection<DbModificationClause>(props);
        return newSetClauses;
    }
}

コンテキストを使用する前に、コード内の任意の場所で以下を実行して、このインターセプターをEFに登録します。

DbInterception.Add(new TemporalTableCommandTreeInterceptor());
21
Matt Ruwe

システムバージョン管理されたテーブルでこのエラーが発生し、システムが維持する列を無視するようにEF構成を設定しただけです。

            Ignore(x => x.SysEndTime);
            Ignore(x => x.SysStartTime);

挿入/更新はDBで機能し、履歴を保持するために必要に応じてこれらの列を更新します。別の方法は、次のように列を設定することです。

Property(x => x.SysEndTime).IsRequired().HasColumnType("datetime2").HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
3
Ihor Bodnarchuk

もう1つの解決策は、テーブルのフィールドにデフォルトの制約を作成することです。

CREATE TABLE [dbo].[Table] (
    [Id]            INT IDENTITY(1, 1)  NOT NULL,
    [Description]   NVARCHAR(100)       NOT NULL,
    [ValidFrom]     DATETIME2(0)        GENERATED ALWAYS AS ROW START HIDDEN CONSTRAINT [Df_Table_ValidFrom] DEFAULT DATEADD(SECOND, -1, SYSUTCDATETIME()),
    [ValidTo]       DATETIME2(0)        GENERATED ALWAYS AS ROW END HIDDEN CONSTRAINT [Df_Table_ValidTo] DEFAULT '9999.12.31 23:59:59.99',
    PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo]),
    CONSTRAINT [Pk_Table] PRIMARY KEY CLUSTERED ([Id] ASC)
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[Table_History]));
GO

コードでは何も変更する必要はありません。

オーバーヘッドなしで、エンティティフレームワークで一時テーブルを使用することができました。

  1. JoséRicardoGarciaが言うように、デフォルトの制約を使用します

    もう1つの解決策は、テーブルのフィールドにデフォルトの制約を作成することです。

    • テーブルを作成する代わりにテーブルを変更するためのスクリプトを次に示します。

      _ALTER TABLE [dbo].[Table]
      ADD ValidFrom DATETIME2(0) GENERATED ALWAYS AS ROW START HIDDEN CONSTRAINT [Df_Table_ValidFrom] DEFAULT DATEADD(SECOND, -1, SYSUTCDATETIME()),
      ValidTo   DATETIME2(0) GENERATED ALWAYS AS ROW END HIDDEN CONSTRAINT [Df_Table_ValidTo] DEFAULT '9999.12.31 23:59:59.99',
      PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
      go
      ALTER TABLE [dbo].[Table]
      SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE=dbo.[TableHistory]))
      GO
      _
  2. Matt Ruweが言うように、edmxで列をIDに切り替えます

    EDMXデザイナの列のプロパティウィンドウで、PERIOD列(私の場合はValidFromとValidTo)のStoreGeneratedPatternをidentityに変更します。 IDのある挿入だけではなく、計算によってEFが挿入と更新の値を更新するため、IDは計算よりも優れています。

  3. 上記の2つの方法は挿入には問題なく機能しているため、エンティティの更新には機能しませんでした。 2つの列が変更されていないことを手動で伝える必要がありました。

    _Entry(existingResult).CurrentValues.SetValues(table);
    Entry(existingResult).Property(x => x.ValidTo).IsModified = false;
    Entry(existingResult).Property(x => x.ValidFrom).IsModified = false;
    _

これで、エンティティが変更されている場合でも、db.SaveChanges()を正常に呼び出して、エラーを取り除くことができます。お役に立てば幸いです。注:私はDbFirstとEF6を使用しています

0
Iannick

期間開始列(ValidFrom)と期間終了列(ValidTo)を作成すると、この問題が修正されるはずです。私たちはこれを行うことができます

ALTER TABLE [dbo].[Table1] ALTER COLUMN [ValidFrom] ADD HIDDEN;
ALTER TABLE [dbo].[Table1] ALTER COLUMN [ValidTo] ADD HIDDEN;

Sys.columnsテーブルでこれらの列に対して非表示の設定を確認できます

SELECT * FROM sys.columns WHERE is_hidden = 1
0
VPP