EF 测试:不同对象的同一外键是否指向同一对象

问题

在一对多关系中,例如一个 Class 对多个 Student ,使用 ToList() 取出一个 List 的 Student 对象,
那么 List 中的 Student 对象外键对应的 Class 到底是各自独立,还是会共用一个对象?
那么分开取出的 Student 们对应的 Class 共用吗?
加上 AsNoTracking() 方法呢?

测试代码

model 定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Class
{
public int Id { get; set; }

public string Number { get; set; }

public List<Student> Students { get; set; } = new List<Student>();
}

public class Student
{
public int Id { get; set; }

public string Name { get; set; }

public Class Class { get; set; }
}

DbContext 的继承和重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AppDbContext : DbContext
{
public DbSet<Class> Classes { get; set; }
public DbSet<Student> Students { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFGetStarted.ConsoleAppFortest.DbForTest;Trusted_Connection=True");
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
}
}

主程序

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public static void Main(string[] args)
{
var dbContext = new AppDbContext();
dbContext.Database.EnsureCreated();

dbContext.Classes.RemoveRange(dbContext.Classes);
dbContext.Students.RemoveRange(dbContext.Students);
dbContext.SaveChanges();

var c = new Class { Number = "001" };
c.Students.Add(new Student()
{
Name = "Hale Lu"
});
c.Students.Add(new Student()
{
Name = "PM Extra"
});
dbContext.Classes.Add(c);
dbContext.SaveChanges();

Console.WriteLine("AsNoTracking:");
var studentsAsNoTracking = dbContext.Students.Include(s => s.Class).AsNoTracking().ToList();
Console.WriteLine("修改前:");
Console.WriteLine("students[0].Class.Number = " + studentsAsNoTracking[0].Class.Number);
Console.WriteLine("students[1].Class.Number = " + studentsAsNoTracking[1].Class.Number);
studentsAsNoTracking[0].Class.Number = "002";
Console.WriteLine("修改后:");
Console.WriteLine("students[0].Class.Number = " + studentsAsNoTracking[0].Class.Number);
Console.WriteLine("students[1].Class.Number = " + studentsAsNoTracking[1].Class.Number);
Console.WriteLine("ToList方法查询结果的外键对象" + (studentsAsNoTracking[0].Class.Number == studentsAsNoTracking[1].Class.Number ? "" : "不") + "共用一个对象");
Console.WriteLine();

var aAsNoTracking = dbContext.Students.Include(s => s.Class).AsNoTracking().First();
var bAsNoTracking = dbContext.Students.Include(s => s.Class).AsNoTracking().Last();
Console.WriteLine("修改前:");
Console.WriteLine("a.Class.Number = " + aAsNoTracking.Class.Number);
Console.WriteLine("b.Class.Number = " + bAsNoTracking.Class.Number);
aAsNoTracking.Class.Number = "003";
Console.WriteLine("修改后:");
Console.WriteLine("a.Class.Number = " + aAsNoTracking.Class.Number);
Console.WriteLine("b.Class.Number = " + bAsNoTracking.Class.Number);
Console.WriteLine("不同的方法查询结果的外键对象" + (aAsNoTracking.Class.Number == bAsNoTracking.Class.Number ? "" : "不") + "共用一个对象");
Console.WriteLine();

Console.WriteLine();
Console.WriteLine("不加AsNoTracking:");
var students = dbContext.Students.Include(s => s.Class).ToList();
Console.WriteLine("修改前:");
Console.WriteLine("students[0].Class.Number = " + students[0].Class.Number);
Console.WriteLine("students[1].Class.Number = " + students[1].Class.Number);
students[0].Class.Number = "002";
Console.WriteLine("修改后:");
Console.WriteLine("students[0].Class.Number = " + students[0].Class.Number);
Console.WriteLine("students[1].Class.Number = " + students[1].Class.Number);
Console.WriteLine("ToList方法查询结果的外键对象" + (students[0].Class.Number == students[1].Class.Number ? "" : "不") + "共用一个对象");
Console.WriteLine();

var a = dbContext.Students.Include(s => s.Class).First();
var b = dbContext.Students.Include(s => s.Class).Last();
Console.WriteLine("修改前:");
Console.WriteLine("a.Class.Number = " + a.Class.Number);
Console.WriteLine("b.Class.Number = " + b.Class.Number);
a.Class.Number = "003";
Console.WriteLine("修改后:");
Console.WriteLine("a.Class.Number = " + a.Class.Number);
Console.WriteLine("b.Class.Number = " + b.Class.Number);
Console.WriteLine("不同的方法查询结果的外键对象" + (a.Class.Number == b.Class.Number ? "" : "不") + "共用一个对象");
Console.WriteLine();
}

运行结果

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
AsNoTracking:
修改前:
students[0].Class.Number = 001
students[1].Class.Number = 001
修改后:
students[0].Class.Number = 002
students[1].Class.Number = 002
ToList方法查询结果的外键对象共用一个对象

修改前:
a.Class.Number = 001
b.Class.Number = 001
修改后:
a.Class.Number = 003
b.Class.Number = 001
不同的方法查询结果的外键对象不共用一个对象


不加AsNoTracking:
修改前:
students[0].Class.Number = 001
students[1].Class.Number = 001
修改后:
students[0].Class.Number = 002
students[1].Class.Number = 002
ToList方法查询结果的外键对象共用一个对象

修改前:
a.Class.Number = 002
b.Class.Number = 002
修改后:
a.Class.Number = 003
b.Class.Number = 003
不同的方法查询结果的外键对象共用一个对象

结论

不加 AsNoTracking() 的情况下,所有的同一外键指向同一对象;
加 AsNoTracking() 的情况下,分开取出的同一外键指向不同的对象,通过 List 方式取出的同一外键指向相同对象。