Entity Frameworkで追加処理を書いていると、目的のオブジェクトでは無い(関連する)オブジェクトも一緒にインサートしようとしてくれちゃいます。

なんででしょ?EFエキスパートな人にとっては常識なんでしょうか?そもそもDBの設計が悪い?

動作がちょっと直観的ではないので、解決策をすぐ忘れます。てことでメモ。

状況の再現

テーブル

SQL Express 2012に適当なDBを作って、PersonとDepartementデーブルを作ります。

DepartmentIDでつながるよう FOREIGN KEYも作成。

部署データとして”IT”部をセットしておきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
USE [TestDB]
GO
DROP TABLE [dbo].[Person]
DROP TABLE [dbo].[Department]


/****** Object:  Table [dbo].[Person]    Script Date: 2015/09/16 10:48:25 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO
-- テーブル作成
CREATE TABLE [dbo].[Department](
	[DepartmentID] [int] NOT NULL,	--キー(自動採番なし)
	[DepartmentName] [nvarchar](50) NOT NULL,
	[Location] [nvarchar](50) NULL,
	CONSTRAINT [PK_Department] PRIMARY KEY CLUSTERED
	(
		[DepartmentID] ASC
	)
) ON [PRIMARY]

CREATE TABLE [dbo].[Person](
	[PersonID] [int] IDENTITY(1,1) NOT NULL,
	[Name] [nvarchar](50) NULL,
	[Age] [int] NULL,
	[DepartmentID] [int] NULL,
	CONSTRAINT [PK_Person] PRIMARY KEY CLUSTERED (
 		[PersonID] ASC
	)
) ON [PRIMARY]

--DepartmentIDでつながるよう FOREIGN KEY作成
ALTER TABLE [dbo].[Person]  WITH CHECK ADD  CONSTRAINT [FK_Person_Department] FOREIGN KEY([DepartmentID])
REFERENCES [dbo].[Department] ([DepartmentID])

--ダミーデータ
INSERT INTO [dbo].[Department] ([DepartmentID], [DepartmentName] ,[Location])
     VALUES (1,'IT','TOKYO')

絵にするとこんな感じ

部署に複数人が所属するっつーベタな構造です。

プロジェクト

プロジェクトはコマンドラインプロジェクトにしました。

Entity

ADO.NET Entity Data Model

Code Firest From database

テーブルは2つ。

できたコード

Visual Studioがいい感じにEntity用のコードを作ってくれます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public partial class Model1 : DbContext
    {
        public Model1()
            : base("name=Model1") {
        }

        public virtual DbSet<Department> Department { get; set; }
        public virtual DbSet<Person> Person { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder) {
        }
    }

    [Table("Person")]
    public partial class Person
    {
        public int PersonID { get; set; }
        [StringLength(50)]
        public string Name { get; set; }
        public int? Age { get; set; }
        public int? DepartmentID { get; set; }

        public virtual Department Department { get; set; }
    }

    [Table("Department")]
    public partial class Department
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public Department()
        {
            Person = new HashSet<Person>();
        }

        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int DepartmentID { get; set; }
        [Required]
        [StringLength(50)]
        public string DepartmentName { get; set; }
        [StringLength(50)]
        public string Location { get; set; }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<Person> Person { get; set; }
    }

(実際は各クラス毎にファイルが作成されます)

テストコード

テストコードはPersonを追加するだけの処理です。

この時、部署情報は呼び出し元からオブジェクトで渡されることを想定して、コンテキスト外で作成しています。

AddPersonがメインの処理です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Program
    {
        static void Main(string[] args) {
            //外から部署を渡されることを再現するため、追加するコンテキスト外でインスタンス作成
            Models.Department IT;
            using (var db = new Models.Model1()) {
                IT = db.Department.FirstOrDefault(d => d.DepartmentID == 1);
            }

            AddPerson("土方 藍亭", 18, IT);
        }
        static void AddPerson(string name,int age, Models.Department department){
            using (var db = new Models.Model1()) {
                db.Database.Log = (sql) => { System.Diagnostics.Debug.Write(sql); };   //SQLを出す
                //Personを追加
                var person = new Models.Person() {
                    Name = name,
                    Age = age,
                    Department = department
                };
                db.Person.Add(person);
                db.SaveChanges();   //ここでエラー
            }
        }
    }

エラー再現

実行すると

1
Violation of PRIMARY KEY constraint 'PK_Department'. Cannot insert duplicate key in object 'dbo.Department'. The duplicate key value is (1).

と例外が発生します。

デバッグ出力を見ると

1
2
3
4
5
INSERT [dbo].[Department]([DepartmentID], [DepartmentName], [Location])
VALUES (@0, @1, @2)
-- @0: '1' (Type = Int32)
-- @1: 'IT' (Type = String, Size = 50)
-- @2: 'TOKYO' (Type = String, Size = 50)

とやろうとしています。

AddしたのはPersonなのになぜかDepartmentを追加しようとしてます。

コンテキスト内にオブジェクトが存在しないのが理由だと思いますが、余計なお世話サマーな気がします。

解決策1 オブジェクトを使わない

オブジェクトを使わないで、IDだけ指定してやるととりあえず動きます。

1
2
3
4
5
6
7
8
9
//Personを追加
                var person = new Models.Person() {
                    Name = name,
                    Age = age,
                    //Department = department   //これやめて
                    DepartmentID = department.DepartmentID  //こうする
                };
                db.Person.Add(person);
                db.SaveChanges();

オブジェクト階層が深くなったり、複合キーになったりすると破綻しそうな気がします。

解決策2 Attachする

コンテキストにdepartmentすでにあるよと教えてやると、インサートしないみたいです。

1
2
3
4
5
6
7
8
9
//Personを追加
                var person = new Models.Person() {
                    Name = name,
                    Age = age,
                    Department = department
                };
                db.Department.Attach(department);   //もうあるからよろしく
                db.Person.Add(person);
                db.SaveChanges();

う~ん、これも関連するオブジェクトが増えるとキツそうです。

解決策3 コンテキスト内で再検索

1
2
3
4
5
6
7
8
//Personを追加
                var person = new Models.Person() {
                    Name = name,
                    Age = age,
                    Department = db.Department.FirstOrDefault(d=>d.DepartmentID==department.DepartmentID)  //ないわー
                };
                db.Person.Add(person);
                db.SaveChanges();

無駄な処理を増やすだけですな。ナイナイ。

結局自分で書くしかないの?

DbContextが非常に優秀なので、この辺の処理もなんとかうまいことやってくれてもいいような気がしますが、

結局のところ、自分でリレーションを意識しつつ書くしかないみたいです。

アトリビュートとかでサクッと解決する方法ないですかねー?