[小技巧]EF Core中如何获取上下文中操作过的实体

原文:[小技巧]EF Core中如何获取上下文中操作过的实体



原文地址:https://www.cnblogs.com/lwqlun/p/10576443.html
作者:Lamond Lu
源代码:https://github.com/lamondlu/EFCoreFindSample


背景介绍#


当我们在工作单元(UnitOfWork)中使用EF/EF Core的时候,为了要保持事务,一个用户操作只能调用一次SaveChange方法,但是有时候一个用户操作需要调用多个Repository,并且他们操作的实体是关联的。这时候在一个Repository中获取另外一个Repository中添加/修改/删除的实体就变成了一个问题。

问题说明#


当前我们做一个学生管理系统,学生和班之间是多对多关系,一个学生可以属于多个班, 因此我们创建了如下的EF上下文。

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
<DIV class=esa-clipboard-button data-clipboard-target="#copy_target_0" data-tips="复制代码">Copy</DIV><wyn>    <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>TestDbContext</SPAN> : <SPAN class=hljs-title>DbContext</SPAN>
    {

        <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-title>TestDbContext</SPAN>(<SPAN class=hljs-params>DbContextOptions<TestDbContext> options</SPAN>) : <SPAN class=hljs-title>base</SPAN>(<SPAN class=hljs-params>options</SPAN>)
        </SPAN>{

        }

        <SPAN class=hljs-keyword>public</SPAN> DbSet<Student> Students { <SPAN class=hljs-keyword>get</SPAN>; <SPAN class=hljs-keyword>set</SPAN>; }

        <SPAN class=hljs-keyword>public</SPAN> DbSet<Group> Groups { <SPAN class=hljs-keyword>get</SPAN>; <SPAN class=hljs-keyword>set</SPAN>; }

        <SPAN class=hljs-function><SPAN class=hljs-keyword>protected</SPAN> <SPAN class=hljs-keyword>override</SPAN> <SPAN class=hljs-keyword>void</SPAN> <SPAN class=hljs-title>OnModelCreating</SPAN>(<SPAN class=hljs-params>ModelBuilder modelBuilder</SPAN>)
        </SPAN>{
            modelBuilder.Entity<StudentGroup>().HasKey(p => <SPAN class=hljs-keyword>new</SPAN> { p.GroupId, p.StudentId });

            <SPAN class=hljs-keyword>base</SPAN>.OnModelCreating(modelBuilder);
        }
    }


    [<SPAN class=hljs-meta>Table(<SPAN class=hljs-meta-string>"Student"</SPAN>)</SPAN>]
    <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>Student</SPAN>
    {
        <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-title>Student</SPAN>(<SPAN class=hljs-params></SPAN>)
        </SPAN>{
            StudentGroups = <SPAN class=hljs-keyword>new</SPAN> List<StudentGroup>();
        }

        [<SPAN class=hljs-meta>Key</SPAN>]
        <SPAN class=hljs-keyword>public</SPAN> Guid StudentId { <SPAN class=hljs-keyword>get</SPAN>; <SPAN class=hljs-keyword>set</SPAN>; }

        <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>string</SPAN> Name { <SPAN class=hljs-keyword>get</SPAN>; <SPAN class=hljs-keyword>set</SPAN>; }

        <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>int</SPAN> Credits { <SPAN class=hljs-keyword>get</SPAN>; <SPAN class=hljs-keyword>set</SPAN>; }

        <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>virtual</SPAN> ICollection<StudentGroup> StudentGroups { <SPAN class=hljs-keyword>get</SPAN>; <SPAN class=hljs-keyword>set</SPAN>; }
    }

    [<SPAN class=hljs-meta>Table(<SPAN class=hljs-meta-string>"Group"</SPAN>)</SPAN>]
    <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>Group</SPAN>
    {
        [<SPAN class=hljs-meta>Key</SPAN>]
        <SPAN class=hljs-keyword>public</SPAN> Guid GroupId { <SPAN class=hljs-keyword>get</SPAN>; <SPAN class=hljs-keyword>set</SPAN>; }

        <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>string</SPAN> GroupName { <SPAN class=hljs-keyword>get</SPAN>; <SPAN class=hljs-keyword>set</SPAN>; }
    }

    [<SPAN class=hljs-meta>Table(<SPAN class=hljs-meta-string>"StudentGroup"</SPAN>)</SPAN>]
    <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>StudentGroup</SPAN>
    {
        <SPAN class=hljs-keyword>public</SPAN> Guid StudentId { <SPAN class=hljs-keyword>get</SPAN>; <SPAN class=hljs-keyword>set</SPAN>; }

        <SPAN class=hljs-keyword>public</SPAN> Guid GroupId { <SPAN class=hljs-keyword>get</SPAN>; <SPAN class=hljs-keyword>set</SPAN>; }

        [<SPAN class=hljs-meta>ForeignKey(<SPAN class=hljs-meta-string>"StudentId"</SPAN>)</SPAN>]
        <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>virtual</SPAN> Student Student { <SPAN class=hljs-keyword>get</SPAN>; <SPAN class=hljs-keyword>set</SPAN>; }

        [<SPAN class=hljs-meta>ForeignKey(<SPAN class=hljs-meta-string>"GroupId"</SPAN>)</SPAN>]
        <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>virtual</SPAN> Group Group { <SPAN class=hljs-keyword>get</SPAN>; <SPAN class=hljs-keyword>set</SPAN>; }
    }

在用户界面上,我们允许用户在添加学生的时候,同时将学生分配到一个班级中。


因此我们的控制器代码如下:

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
<DIV class=esa-clipboard-button data-clipboard-target="#copy_target_1" data-tips="复制代码">Copy</DIV><wyn>    <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>StudentController</SPAN> : <SPAN class=hljs-title>ControllerBase</SPAN>
    {
        <SPAN class=hljs-keyword>private</SPAN> StudentManager _studentManager = <SPAN class=hljs-literal>null</SPAN>;

        <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-title>StudentController</SPAN>(<SPAN class=hljs-params>StudentManager studentManager</SPAN>)
        </SPAN>{
            _studentManager = studentManager;
        }

        <SPAN class=hljs-comment>// GET api/values</SPAN>
        [<SPAN class=hljs-meta>HttpPost</SPAN>]
        <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> IActionResult <SPAN class=hljs-title>Post</SPAN>(<SPAN class=hljs-params>[FromBody]AddStudentDTO dto</SPAN>)
        </SPAN>{
            <SPAN class=hljs-keyword>try</SPAN>
            {
                _studentManager.AddStudent(dto.Name, dto.GroupId);

                <SPAN class=hljs-keyword>return</SPAN> StatusCode(<SPAN class=hljs-number>201</SPAN>);
            }
            <SPAN class=hljs-keyword>catch</SPAN>
            {
                <SPAN class=hljs-keyword>return</SPAN> StatusCode(<SPAN class=hljs-number>500</SPAN>, <SPAN class=hljs-keyword>new</SPAN> { message = <SPAN class=hljs-string>"Unexpected Issue."</SPAN> });
            }
        }
    }

为了完成我们的业务,在StudentManagerAddStudent方法中,我们需要完成两步操作



  1. 添加学生信息
  2. 将学生分配给指定班

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<DIV class=esa-clipboard-button data-clipboard-target="#copy_target_2" data-tips="复制代码">Copy</DIV><wyn>    <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>StudentManager</SPAN>
    {
        <SPAN class=hljs-keyword>private</SPAN> IUnitOfWork _unitOfWork;

        <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-title>StudentManager</SPAN>(<SPAN class=hljs-params>IUnitOfWork unitOfWork</SPAN>)
        </SPAN>{
            _unitOfWork = unitOfWork;
        }

        <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>void</SPAN> <SPAN class=hljs-title>AddStudent</SPAN>(<SPAN class=hljs-params><SPAN class=hljs-keyword>string</SPAN> studentName, Guid groupId</SPAN>)
        </SPAN>{
            <SPAN class=hljs-keyword>var</SPAN> newStudentId = Guid.NewGuid();

            _unitOfWork.StudentRepository.AddStudent(newStudentId, studentName);
            _unitOfWork.GroupRepository.AssignStudentToGroup(newStudentId, groupId);

            _unitOfWork.Commit();
           
        }
    }

这里我们使用StudentRepositoryAddStudent方法来完成保存学生信息,使用GroupRepositoryAssignStudentToGroup方法来将学生分配给班级。



这里,其实不应该将保存学生信息和分配班级都放在这里,可以使用事件发布/订阅将其分配班级的逻辑移动到别处。


针对保存学生信息的操作,代码很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<DIV class=esa-clipboard-button data-clipboard-target="#copy_target_3" data-tips="复制代码">Copy</DIV><wyn>    <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-class><SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>StudentRepository</SPAN> :</SPAN> IStudentRepository
    {
        <SPAN class=hljs-keyword>private</SPAN> TestDbContext _dbContext;

        <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-title>StudentRepository</SPAN><SPAN class=hljs-params>(TestDbContext dbContext)</SPAN>
        </SPAN>{
            _dbContext = dbContext;
        }

        <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>void</SPAN> <SPAN class=hljs-title>AddStudent</SPAN><SPAN class=hljs-params>(Guid studentId, <SPAN class=hljs-built_in>string</SPAN> name)</SPAN>
        </SPAN>{
            _dbContext.Students.Add(<SPAN class=hljs-keyword>new</SPAN> Student
            {
                StudentId = studentId,
                Name = name,
                Credits = <SPAN class=hljs-number>0</SPAN>
            });
        }
    }

但是当我们继续编写AssignStudentToGroup方法时就会遇到问题,我们该如何获取到前面方法中添加的Student实体?


这时候,有同学会去尝试



_dbContext.Students.Where(p=>p.StudentId = studentId)


你会发现它获取不到你想要的对象,原因是这条语句进行的是数据库查询,当前新增的Student对象还没有保存到数据库


那么如何解决这个问题呢?这里有2种解决方案



  • ChangeTracker上获取
  • 使用Find方法获取

ChangeTracker上获取#


ChangeTracker是EF/EF Core中的核心对象,在这个对象中记录了当前EF上下文,操作过的所有实体,实体状态及实体属性的变更。


ChangeTracker中的Entries泛型方法可以帮助我们获取到当前上下文中操作过的指定类型实体集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<DIV class=esa-clipboard-button data-clipboard-target="#copy_target_4" data-tips="复制代码">Copy</DIV><wyn>    public <SPAN class=hljs-keyword>void</SPAN> AssignStudentToGroup(Guid studentId, Guid groupId)
    {
        Student student = _dbContext.ChangeTracker.Entries<Student>().FirstOrDefault(<SPAN class=hljs-function><SPAN class=hljs-params>p</SPAN> =></SPAN> p.Entity.StudentId == studentId).Entity;;

        <SPAN class=hljs-keyword>if</SPAN> (student == <SPAN class=hljs-literal>null</SPAN>)
        {
            <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> KeyNotFoundException(<SPAN class=hljs-string>"The student id could not be found."</SPAN>);
        }

        student.StudentGroups.Add(<SPAN class=hljs-keyword>new</SPAN> StudentGroup
        {      
            StudentId = studentId,
            GroupId = groupId
        });
    }

但是这样写会出现一个问题,如果我想为一个数据库中已经存在的学生分配班级,调用这个方法就会出现问题,因为该实体还未加载到ChangeTracker中, 所以我们这里还需要使用_dbContext.Students.First方法进行数据库查询.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<DIV class=esa-clipboard-button data-clipboard-target="#copy_target_5" data-tips="复制代码">Copy</DIV><wyn>    public <SPAN class=hljs-keyword>void</SPAN> AssignStudentToGroup(Guid studentId, Guid groupId)
    {
        Student student;

        <SPAN class=hljs-keyword>if</SPAN> (_dbContext.ChangeTracker.Entries<Student>().Any(<SPAN class=hljs-function><SPAN class=hljs-params>p</SPAN> =></SPAN> p.Entity.StudentId == studentId))
        {
            student = _dbContext.ChangeTracker.Entries<Student>().First(<SPAN class=hljs-function><SPAN class=hljs-params>p</SPAN> =></SPAN> p.Entity.StudentId == studentId).Entity;
        }
        <SPAN class=hljs-keyword>else</SPAN> <SPAN class=hljs-keyword>if</SPAN> (_dbContext.Students.Any(<SPAN class=hljs-function><SPAN class=hljs-params>p</SPAN> =></SPAN> p.StudentId == studentId))
        {
            student = _dbContext.Students.First(<SPAN class=hljs-function><SPAN class=hljs-params>p</SPAN> =></SPAN> p.StudentId == studentId);
        }
        <SPAN class=hljs-keyword>else</SPAN>
        {
            <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> KeyNotFoundException(<SPAN class=hljs-string>"The student id could not be found."</SPAN>);
        }

        student.StudentGroups.Add(<SPAN class=hljs-keyword>new</SPAN> StudentGroup
        {
            StudentId = studentId,
            GroupId = groupId
        });
    }

至此,整个方法的修改就完成了。如果你觉着这种方式比较繁琐,请继续看下面的Find方法。

使用Find方法#


EF/EF Core中其实还提供了一个Find方法,以下是该方法的方法签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<DIV class=esa-clipboard-button data-clipboard-target="#copy_target_6" data-tips="复制代码">Copy</DIV><wyn>    // Summary:
    //     Finds an <SPAN class=hljs-keyword>entity</SPAN> <SPAN class=hljs-keyword>with</SPAN> the given primary key values. <SPAN class=hljs-keyword>If</SPAN> an <SPAN class=hljs-keyword>entity</SPAN> <SPAN class=hljs-keyword>with</SPAN> the given
    //     primary key values <SPAN class=hljs-keyword>is</SPAN> being tracked by the <SPAN class=hljs-keyword>context</SPAN>, <SPAN class=hljs-keyword>then</SPAN> it <SPAN class=hljs-keyword>is</SPAN> returned immediately
    //     without making a request <SPAN class=hljs-keyword>to</SPAN> the database. Otherwise, a query <SPAN class=hljs-keyword>is</SPAN> made <SPAN class=hljs-keyword>to</SPAN> the database
    //     <SPAN class=hljs-keyword>for</SPAN> an <SPAN class=hljs-keyword>entity</SPAN> <SPAN class=hljs-keyword>with</SPAN> the given primary key values <SPAN class=hljs-keyword>and</SPAN> this <SPAN class=hljs-keyword>entity</SPAN>, <SPAN class=hljs-keyword>if</SPAN> found, <SPAN class=hljs-keyword>is</SPAN>
    //     attached <SPAN class=hljs-keyword>to</SPAN> the <SPAN class=hljs-keyword>context</SPAN> <SPAN class=hljs-keyword>and</SPAN> returned. <SPAN class=hljs-keyword>If</SPAN> no <SPAN class=hljs-keyword>entity</SPAN> <SPAN class=hljs-keyword>is</SPAN> found, <SPAN class=hljs-keyword>then</SPAN> <SPAN class=hljs-keyword>null</SPAN> <SPAN class=hljs-keyword>is</SPAN> returned.
    //
    // Parameters:
    //   keyValues:
    //     The values <SPAN class=hljs-keyword>of</SPAN> the primary key <SPAN class=hljs-keyword>for</SPAN> the <SPAN class=hljs-keyword>entity</SPAN> <SPAN class=hljs-keyword>to</SPAN> be found.
    //
    // Returns:
    //     The <SPAN class=hljs-keyword>entity</SPAN> found, <SPAN class=hljs-keyword>or</SPAN> <SPAN class=hljs-keyword>null</SPAN>.
    public virtual TEntity Find([CanBeNullAttribute] params object[] keyValues);

从这个Find方法的注释中,我们可以了解到,Find方法可以根据实体主键查询实体。但是它的优点是,它会优先去ChangeTracker中查找,如果查找不到才会生成查询语句,进行数据库查询。


由此,我们可以使用Find方法修改AssignStudentToGroup方法,看起来比之前的代码简化了不少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<DIV class=esa-clipboard-button data-clipboard-target="#copy_target_7" data-tips="复制代码">Copy</DIV><wyn>    <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>void</SPAN> <SPAN class=hljs-title>AssignStudentToGroup</SPAN><SPAN class=hljs-params>(Guid studentId, Guid groupId)</SPAN>
    </SPAN>{
        Student student = _dbContext.Students.Find(studentId);

        <SPAN class=hljs-keyword>if</SPAN> (student == <SPAN class=hljs-keyword>null</SPAN>)
        {
            <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> KeyNotFoundException(<SPAN class=hljs-string>"The student id could not be found."</SPAN>);
        }

        student.StudentGroups.Add(<SPAN class=hljs-keyword>new</SPAN> StudentGroup
        {
            StudentId = studentId,
            GroupId = groupId
        });
    }