SqlException:ID 列に明示的な値を挿入できません

ID 列を持つテーブルがあり、レコードを挿入するときに ID 列の値を指定しようとすると、次の例外が発生します:

このエラーは、テーブルに ID 列があり、それに値を設定しようとしていることを意味します。このような ID 列がある場合、その値は挿入時に自動的に生成されるため、この列に値を渡すことができないのはなぜですか。

たとえば、テーブルに次の定義があるとします:

CREATE TABLE [dbo].[Movies](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Name] [nvarchar](500) NOT NULL,
	[YearOfRelease] [int] NOT NULL,
	[Description] [nvarchar](500) NOT NULL,
	[Director] [nvarchar](100) NOT NULL,
	[BoxOfficeRevenue] [decimal](18, 2) NOT NULL,
 CONSTRAINT [PK_Movies] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
Code language: SQL (Structured Query Language) (sql)

この問題を解決するためのいくつかの異なる解決策を紹介します。

注:以下のソリューションは、EF Core を使用したコード例を示しています。 ADO.NET または別の ORM (Dapper など) を使用している場合は、同じソリューションが機能します (コードが異なるだけです)。

オプション 1 – 挿入時に ID 列を指定しない

最初のオプションは最も簡単です – ID 列の値を設定しようとしないでください:

using (var context = new StreamingServiceContext(connectionString))
{
	context.Movies.Add(new Movie()
	{
		//Id = 20,
		Name = "Godzilla",
		Description = "Nuclear lizard fights monsters",
		Director = "Gareth Edwards",
		YearOfRelease = 2014,
		BoxOfficeRevenue = 529_000_000.00m
	});

	context.SaveChanges();
}
Code language: C# (cs)

レコードを挿入すると、SQL Server が値を生成し、EF Core が自動生成された値でプロパティを更新します。

オプション 2 – IDENTITY_INSERT をオンにする

場合によっては、ID を自動生成するのではなく、明示的に設定したい場合があります。この場合、次のように IDENTITY_INSERT を有効にする必要があります:

using (var context = new StreamingServiceContext(connectionString))
{
	using (var transaction = context.Database.BeginTransaction())
	{
		context.Movies.Add(new Movie()
		{
			Id = 20,
			Name = "Godzilla",
			Description = "Nuclear lizard fights monsters",
			Director = "Gareth Edwards",
			YearOfRelease = 2014,
			BoxOfficeRevenue = 529_000_000.00m
		});

		context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT dbo.Movies ON;");
		context.SaveChanges();
		context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT dbo.Movies OFF;");
		transaction.Commit();
	}
}
Code language: C# (cs)

注:EF Core を使用している場合、これを機能させるには、トランザクション内でクエリを実行する必要があります。

IDENTITY_INSERT は、セッションごとに一度に 1 つのテーブルに対してのみオンにできます。

一度に 2 つのテーブルに対して IDENTITY_INSERT を有効にしようとするとします。

context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT dbo.Movies ON;");
context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT dbo.Actors ON;");
Code language: C# (cs)

次の例外が発生します:

この制限は、セッションごとにのみ適用されます。他のセッションがそのセッションで Actors テーブルの IDENTITY_INSERT をオンにした場合、同時に別のセッションの映画の IDENTITY_INSERT をオンにすることができます。

オプション 3 – 列から IDENTITY 仕様を削除する

開発環境にいて、この ID 挿入例外に遭遇するまで ID 列があることに気付かなかった場合は、列から IDENTITY 仕様を削除したいだけかもしれません。

EF Core を使用してテーブルを作成している場合は、DatabaseGenerated(DatabaseGeneratedOption.None)) 属性を使用して、列が ID 列であってはならないことを指定します。

using System.ComponentModel.DataAnnotations.Schema;

public class Movie
{
	[Key]
	[DatabaseGenerated(DatabaseGeneratedOption.None)]
	public int Id { get; set; }
	
	//rest of class
}
Code language: C# (cs)

EF Core は、このスキーマの変更を正しく処理しません。これをスキーマの変更として行うのではなく、テーブルを作成した移行をやり直してください。

たとえば、2 つの移行 (Database_v1_Init と Database_v2_CreateMoviesTable) があり、Movies テーブルを変更して ID 列がないようにしたいとします。 Database_v2_CreateMoviesTable の移行をやり直すには、次の手順を実行します:

  • Database_v1_Init に移行:
dotnet ef database update Database_v1_Init
Code language: PowerShell (powershell)
  • 最後の移行 (この場合は Database_v2_CreateMoviesTable) を削除します:
dotnet ef migrations remove
Code language: PowerShell (powershell)

注:モデルのスナップショット ファイルが同期しなくなるため、移行ファイルだけを削除しないでください。

  • [DatabaseGenerated(DatabaseGeneratedOption.None)] 属性を Movie.Id プロパティに追加します。
public class Movie
{
	[Key]
	[DatabaseGenerated(DatabaseGeneratedOption.None)]
	public int Id { get; set; }
Code language: C# (cs)
  • 移行 Database_v2_CreateMoviesTable を再作成します:
dotnet ef migrations add Database_v2_CreateMoviesTable
Code language: PowerShell (powershell)
  • _Database_v2_CreateMoviesTable.cs で生成された移行ソース コードを確認します。 まず、IDENTITY 仕様で列を作成していないことがわかります。次に、この移行で実行する必要があるのは、Movies テーブルの作成だけです。他の処理を行っている場合は、モデルのスナップショット ファイルが無効な状態になった可能性があります (おそらく、移行ファイルを手動で削除したため)。
public partial class Database_v2_CreateMoviesTable : Migration
{
	protected override void Up(MigrationBuilder migrationBuilder)
	{
		migrationBuilder.CreateTable(
			name: "Movies",
			columns: table => new
			{
				Id = table.Column<int>(type: "int", nullable: false),
				Name = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
				YearOfRelease = table.Column<int>(type: "int", nullable: false),
				Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
				Director = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
				BoxOfficeRevenue = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
			},
			constraints: table =>
			{
				table.PrimaryKey("PK_Movies", x => x.Id);
			});
			
			//rest of class not shown
}
Code language: C# (cs)
  • 移行を適用:
dotnet ef database update Database_v2_CreateMoviesTable
Code language: PowerShell (powershell)

id 列に IDENTITY の指定がなくなったので、id の値を指定しながらテーブルにレコードを挿入できます。

免責事項:これは本番環境には適用されません。テーブルを削除して再作成すると、データが失われます。開発環境にいて、データが失われても構わない場合にのみ、このアプローチをお勧めします。

オプション 4 – 更新を行う場合は、最初にレコードをフェッチする

挿入ではなく更新を行うには、最初にレコードを取得します。それ以外の場合、SaveChanges() を呼び出すと、EF Core によって挿入ステートメントが生成されます。 ID 列の値を指定しようとすると、ID 挿入例外が発生します。

最初に取得してレコードを更新する方法は次のとおりです。

using (var context = new StreamingServiceContext(connectionString))
{
	var movie = await context.Movies.FirstOrDefaultAsync(t => t.Id == 20);
	movie.Description = "Nuclear lizard fights monsters";
	
	context.SaveChanges();
}
Code language: C# (cs)