EntityFramework Core进行关系数据的增删改查

参考资料:
微软MVP杨旭教程:https://www.bilibili.com/video/BV1xa4y1v7rR?p=7

  • 添加关系数据
    • 用其它Context来添加关系数据
    • Attach()与变化追踪
    • Add()、Update()、Attach()处理主键的对比
  • 加载关联数据
    • 预加载,Include()ThenInclude()
    • 显式加载,Entry()Collection()Reference()Load()Query()
    • 懒加载
    • 使用关联数据的一些属性作为查询条件
    • 多对多关系查询
  • 修改关系数据
    • 离线状态
    • 设置多对多的关系
    • 删除多对多的关系
    • 修改多对多关系
    • 设置一对一关系
    • 修改一对一关系

添加关系数据

目前一个League对应多个Club,一个Club又对应多个Player。

计划从数据库中查出League-"Serie A",然后往Serie A这个联赛中添加一个Club。观察League类中只有Id、Name、Country三个简单的属性,显然无法给League添加一个Club,但可以给Club添加一个League。可以new一个Club,把它的League属性设为前面查出来的Serie A,它们就关联起来了。

using var context = new DemoDbContext();

var serieA = context.Leagues.SingleOrDefault(x => x.Name == "Serie A");
var juventus = new Club
{
    League = serieA,
    Name = "Juventus",
    City = "Torino",
    DateOfEstablished = new DateTime(1897, 11, 1)
};

context.Clubs.Add(juventus);
var count = context.SaveChanges();
Console.WriteLine(count);

这就是第一种方法:在新建立的对象上指定导航属性的值,或者它引用的对象。

第二种,一个Club,它有多个球员Player,如何建立Player到Club的关系?Player类中没有与Club有关的属性,但Club类中有一个集合类的导航属性到Player,可以通过它建立Player和Club的关系。在new Club的时候,把Player属性赋上。可以向下面代码一样直接new新的Player,也可以查询出已有的player,感觉实际应用中应该基本都是查询出来的Player。

修改上面代码:

var juventus = new Club
{
    League = serieA,
    Name = "Juventus",
    City = "Torino",
    DateOfEstablished = new DateTime(1897, 11, 1),
    Players = new List
    {
        new Player
        {
            Name = "C. Ronaldo",
            DateOfBirth = new DateTime(1985, 2, 5)
        }
    }
};

然后再尝试往现有的俱乐部中添加一个人员。

Club中有一个集合类导航属性Players,可以直接通过它的Add方法来添加一个新的球员:

var juventus = context.Clubs.SingleOrDefault(x => x.Name == "Juventus");

juventus.Players.Add(new Player
{
    Name = "Gonzalo Higuain",
    DateOfBirth = new DateTime(1987, 12, 10)
});

var count = context.SaveChanges();

因为Club是被Context变化追踪的,上一篇中写的取消追踪的代码被我注释掉了,所以一旦添加了新的球员,Context就会知道这个球员是新加的,并且不用再new的Player里设置外键,因为直接用的juventus.Players.Add()。

注意:真实的项目中一般不使用Single()和First(),而是使用SingleOrDefault()和FirstOrDefault()。

用其它Context来添加关系数据

我们尝试用另一个新new出来的Context来给尤文图斯俱乐部添加球员。当然是先用原来的Context查出俱乐部来,新Context并没有追踪原Context查出来的Club。

using var context = new DemoDbContext();

var juventus = context.Clubs.SingleOrDefault(x => x.Name == "Juventus");

juventus.Players.Add(new Player
{
    Name = "Matthijs de Ligt",
    DateOfBirth = new DateTime(1999, 8, 12)
});

{
    using var newContext = new DemoDbContext();
    newContext.Clubs.Update(juventus);

    var count = newContext.SaveChanges();
    Console.WriteLine(count);
}

juventus对新new的Context来说就相当于离线的数据,就像是JSON里面传出来的。修改这种数据,应该使用Update()方法。执行UpDate()方法时,它就会发现new的Player是没有Id的,它是一个新的Player,而且跟juventus有一个外键的关系。这些对新Context来说都可以理解出来。

运行一下,发现存在一些问题:

EntityFramework Core进行关系数据的增删改查_第1张图片

我们只想添加一个球员,但它把整个尤文图斯俱乐部都更新(Update)了一遍。

Attach()与变化追踪

通过调用DbSet或者Context上的Add()、Update()、Remove()方法,都会有变化追踪这种效果,实际上还有Attach()方法,这个方法也经常使用。Attach是附加的意思,我们把前面代码中的Update改成Attach,来把这个俱乐部附加上。

附加上这个尤文图斯对象后,这个对象处于未修改的状态,所以Context不会对它进行修改,但却已经追踪了。Context会发现这个对象中有一个Player,是没有主键的,所以这个Player属于新增的对象,Context就会针对这个新增的对象生成INSERT语句,然后插入到数据库里。我们在new一个新Player尝试一下:

juventus.Players.Add(new Player
{
    Name = "Miralem Pjanic",
    DateOfBirth = new DateTime(1990, 4, 2)
});

{
    using var newContext = new DemoDbContext();
    newContext.Clubs.Attach(juventus);

    var count = newContext.SaveChanges();
    Console.WriteLine(count);
}

再运行,发现没有Update语句了。INSERT中把新Player的ClubId设为了2。

尝试一下能否直接赋予外键。我们给C罗添加一个简历Resume,已知C罗的PlayerId为1。

var resume = new Resume
{
    PlayerId = 1,
    Description = "..."
};

context.Resumes.Add(resume);

var count = context.SaveChanges();

Console.WriteLine(count);

执行后发现是可以的。

Add()、Update()、Attach()处理主键的对比

EntityFramework Core进行关系数据的增删改查_第2张图片

参考上表,Add()传进去的数据有主键的话,也就是主键这个属性有值的话,就是把这条数据添加到数据库里。但如果这个表的主键我们设置为自动生成的话,我们又手动赋了主键,就会抛出一个异常。

如果Update()方法传进去的数据有主键的话,就会修改原来的主键,换成新赋的主键,而不是新增。

但如果有主键的数据使用Attach()方法附加到Context上,就不会发生任何变化。

没有主键的数据使用这三个方法,都是添加数据。包括它们关联的数据,如果没有主键,也都是这个效果。

加载关联数据

加载关联数据有三种方法

  • 预加载,Eager loading
  • 显式加载,Explicit loading
  • 懒加载,Lazy loading

预加载,Include()ThenInclude()

尝试查询所有俱乐部,并且顺便把它所属的联赛也查询出来,需要使用Include()

var clubs = context.Clubs
    .Where(x => x.Id > 0)
    .Include(x => x.League)
    .ToList();

同时可以添加限定条件,使用Where()。注意顺序,要先Where(),再Include(),最后可以ToList()或者FirstOrDefault()等。

如果用DbSet的Find()方法,就无法使用Include()

如果想顺便把俱乐部里的球员也查出来,就再加一个Include()

var clubs = context.Clubs
    .Where(x => x.Id > 0)
    .Include(x => x.League)
    .Include(x => x.Players)
    .ToList();

这样就把Club关联的两个属性:Leagues和Players都加进来了。如果还想再把Player的关联属性也加进来,当然就不能再使用Include(),因为Include()是针对Club的关联属性。所以对Player,可以使用另外一个方法叫ThenInclude(),相当于级联地添加关联数据:

var clubs = context.Clubs
    .Where(x => x.Id > 0)
    .Include(x => x.League)
    .Include(x => x.Players)
        .ThenInclude(y => y.Resume)
    .ToList();

如果想把Players的GamePlayers这个关联属性也添加进来,这时候就要注意了。如果继续写ThenInclude(),这个Then就是针对Resume的,而不是Players的了。可以再写一次Include()ThenInclude()这种语句,相当于把后面的ThenInclude()关联到新的Include()

var clubs = context.Clubs
    .Where(x => x.Id > 0)
    .Include(x => x.League)
    .Include(x => x.Players)
        .ThenInclude(y => y.Resume)
    .Include(x => x.Players)
        .ThenInclude(y => y.GamePlayers)
    .ToList();

如果还要继续查GamePlayer的关联属性,这时候就可以在后面用ThenInclude()了,它关联的是GamePlayer:

var clubs = context.Clubs
    .Where(x => x.Id > 0)
    .Include(x => x.League)
    .Include(x => x.Players)
        .ThenInclude(y => y.Resume)
    .Include(x => x.Players)
        .ThenInclude(y => y.GamePlayers)
            .ThenInclude(z => z.Game)
    .ToList();

可以看一下这个查询结果生成的SQL语句:

EntityFramework Core进行关系数据的增删改查_第3张图片

再使用另一种形式,我们只查询出部分表的特定的部分属性,只获取我们想要的部分信息,而且我们想要对我们的关联数据再按条件查询,这就用到了Select(),还使用了匿名类,其中 关联数据还使用了Where()

var info = context.Clubs
    .Where(x => x.Id > 0)
    .Select(x => new
    {
        x.Id,
        LeagueName = x.League.Name,
        x.Name,
        Players = x.Players
            .Where(p => p.DateOfBirth > new DateTime(1990, 1, 1))
    })
    .ToList();
    // Context无法变化追踪匿名类,只能追踪在它识别的在它里面声明的或者未声明的关联类

这种查询结果是一个匿名类,无法被Context变化追踪。但其中的Players如果发生任何变化,是可以识别出来的。

显式加载,Entry()Collection()Reference()Load()Query()

再举一个例子。先把一个Club查出来,但不包含它的任何关联数据,这样这个Club就在内存里了:

var info = context.Clubs.First();   // 查询之后info就在内存中了

然后再把它关联的数据查出来。看一下Club类,它关联的数据有League和Players这两个,一个是单个的,一个是集合的。

对集合的Players,把info放在Entry()(入口)的参数中。因为关联属性Players是一个集合,所以用Collection()方法,再使用Load()就会进行查询。

context.Entry(info)
    .Collection(x => x.Players)
    .Load();

对这种集合导航属性,我们加载的时候还可以加上一些过滤条件,在Collection()后面使用Query(),再在Query()后加上Where()条件:

context.Entry(info)
    .Collection(x => x.Players)
    .Query()
    .Where(x => x.DateOfBirth > new DateTime(1990, 1, 1))
    .Load();

对单个的关联属性,不再使用Collection(),而是使用Reference():

context.Entry(info)
    .Reference(x => x.League)
    .Load();

这种显式加载有一个缺点,只能针对单个数据进行加载,比如这里的info就是一个Club。如果式针对一个List,则无法使用这种形式。

懒加载

懒加载在EF Core中默认是关闭的,可以手动开启,但会引起很多问题,使用的很少。

使用关联数据的一些属性作为查询条件

查Club的时候,可以使用它的关联属性League的Name属性作为查询条件:

var data = context.Clubs
    .Where(x => x.League.Name.Contains("e"))
    .ToList();

多对多关系查询

Game和Player之间通过一个中间类GamePlayer形成了多对多关系。无法直接查出多对多关系,但可以间接查出来。可以在查Player时把GamePlayer给Include进来,再通过它查Game的数据。

我们先建立一个这种关系。

using var context = new DemoDbContext();

var player = context.Players
    .Where(p => p.Name == "C. Ronaldo").FirstOrDefault();

var game = new Game
{
    Round = 2
};

// context.Games.Add(game);

var gamePlayer = new GamePlayer
{
    Game = game,
    Player = player
};

context.GamePlayers.Add(gamePlayer);

var count = context.SaveChanges();

Console.WriteLine(count);

可以看到其中我们Add Game的语句被注释掉了,但最终Context还是为我们Add了一条Game数据,比较智能。然后进行间接多对多查询:

var data = context.Players
    .Where(p => p.Id > 0)
    .Include(p => p.GamePlayers)
        .ThenInclude(x => x.Game)
    .ToList();

EntityFramework Core进行关系数据的增删改查_第4张图片

假设我们的GamePlayer并没有在Context中声明为DbSet,可以使用Context.Set()来查它:

var gamePlayers = context.Set()
    .Where(x => x.Player.Id > 0)
    .ToList();

修改关系数据

查出一个Club,并且查出它关联的League,然后修改这个League的Name:

using var context = new DemoDbContext();

var club = context.Clubs.Include(x => x.League).First();

club.League.Name += "@";

var count = context.SaveChanges();

Console.WriteLine(count);

EntityFramework Core进行关系数据的增删改查_第5张图片

可以看到EF Core非常智能,它追踪到了League,并且生成了相应的Update语句。

离线状态

查一下Game,Game和Player时多对多的关系,中间通过一个GamePlayer关联,我们都查出来。然后取出这个Game的第一个GamePlayer,作为需要修改的对象,改一下它的名称。

因为我们要模拟离线状态,比如说是从JSON中获取的数据,所以我们new一个新的Context来修改,并使用Update()方法。

using var context = new DemoDbContext();

var game = context.Games
    .Include(x => x.GamePlayers)
        .ThenInclude(y => y.Player)
    .First();

var firstPlayer = game.GamePlayers[0].Player;
firstPlayer.Name += "$";

{
    var newContext = new DemoDbContext();
    newContext.Players.Update(firstPlayer);
    newContext.SaveChanges();
}

使用Update方法会更新firstPlayer上除了主键之外所有的属性,实际上我们可以接受。

EntityFramework Core进行关系数据的增删改查_第6张图片

我们意愿是修改一条数据,但实际上出现了两个Update操作。如果我们这个Game下有两个GamePlayer的话,可能会出现三个Update操作,一个是对Game的,两个是对Player的。UpDate()会把它的参数的所有关联的对象都更新一遍。

解决方法:

{
    var newContext = new DemoDbContext();
    newContext.Entry(firstPlayer).State = EntityState.Modified;
    // newContext.Players.Update(firstPlayer);
    newContext.SaveChanges();
}

用COntext的Entry()方法,把firstPlayer传进去,Entry上面有一个状态(state)字段,把它设置为EntityState.Modified就行了。相当于手动设置它的状态,这样它就只会修改这一个firstPlayer数据,我理解为它不再级联地追踪其他数据。

EntityFramework Core进行关系数据的增删改查_第7张图片

再运行,发现只有一个查询语句和一个Update语句了。

设置多对多的关系

我们创建Game和Player的多对多的关系。直到GameId和PlayerId,就可以new一个它们之间的GamePlayer,然后把外键都设上值就可以了。

using var context = new DemoDbContext();

var gamePlayer = new GamePlayer
{
    GameId = 1,
    PlayerId = 3
};

context.Add(gamePlayer);
context.SaveChanges();

UTOOLS1593159993496.png

可以看到即使我们没有用GamePlayers这个DbSet,也可以直接添加数据。

第二种情况,我们先把Game查询出来,Game中有GamePlayers这个导航属性,用这个导航属性添加GamePlayer就可以。这样因为是通过Game来添加,就不必设置GameId,只设置PlayerId即可:

var game = context.Games.FirstOrDefault();

game.GamePlayers.Add(new GamePlayer
{
    PlayerId = 4
});

删除多对多的关系

我们直到GameId为1和PlayerId为4的Game和Player存在关系,想要删除这个关系,可以new一个GamePlayer,把它两个外键分别设为1和4,然后调用``Remove()`方法把这个GamePlayer删掉。更好的办法是从数据库中查出1和4的这个关系然后删掉。

using var context = new DemoDbContext();

var gamePlayer = context.GamePlayers
    .Where(x => x.GameId == 1 && x.PlayerId == 4)
    .FirstOrDefault();

context.GamePlayers.Remove(gamePlayer);

context.SaveChanges();

修改多对多关系

比如把GameId=1和PlayerId=4的关系改为GameId=1和PlayerId=3的关系,实际上是不行的,我们不能通过EF Core修改它的联合主键值,可以通过SQL语句来修改。如果硬要使用EF Core的话,可以分成两步,首先删除原来1和4的关系,再建立新关系。

using var context = new DemoDbContext();

var gamePlayer = context.GamePlayers
    .Where(x => x.GameId == 1 && x.PlayerId == 4)
    .FirstOrDefault();

context.GamePlayers.Remove(gamePlayer);

var game = context.Games
    .Where(x => x.Id == 1)
    .FirstOrDefault();

var player = context.Players
    .Where(x => x.Id == 3)
    .FirstOrDefault();

var newGamePlayer = new GamePlayer
{
    GameId = game.Id,
    PlayerId = player.Id
};

context.GamePlayers.Add(newGamePlayer);
context.SaveChanges();

设置一对一关系

从数据库中取出一个Player,给它搞一个Resume,新new的也可以,因为它处于变化追踪的状态,新new的Resume不必再手动添加到数据库,可以直接就顺便保存进去了:

var player = context.Players
    .Where(x => x.Id == 2)
    .FirstOrDefault();

player.Resume = new Resume
{
    Description = "1234"
};

context.SaveChanges();

离线状态:

using var context = new DemoDbContext();

var player = context.Players
    .AsNoTracking()
    .OrderBy(x => x.Id)
    .LastOrDefault();

player.Resume = new Resume
{
    Description = "4321"
};

{
    using var newContext = new DemoDbContext();
    newContext.Attach(player);
    newContext.SaveChanges();
}

这里如果不用Attach()而是用Update()的话,会把Player的除主键外所有属性都更新。所以选择Attach()。EF Core知道新的Resume是刚生成的,数据库中还没有,所以即使使用Attach(),依然会把新Resume执行INSERT语句持久化到数据库中。

修改一对一关系

但如果我们想修改某个Player的Resume,数据库中的Player已经有Resume,再给new一个新的Resume,按照上面操作来执行,就会报异常:

using var context = new DemoDbContext();

var player = context.Players
    .AsNoTracking()
    .OrderBy(x => x.Id)
    .LastOrDefault();

player.Resume = new Resume
{
    Description = "12121212121212"
};

{
    using var newContext = new DemoDbContext();
    newContext.Attach(player);
    newContext.SaveChanges();
}

EntityFramework Core进行关系数据的增删改查_第8张图片

新赋的值的索引在数据库中已经存在。我猜想是因为查询的时候没有把Player的Resume查询出来。我们用另外一种方式来修改Player的Resume。不再使用离线模式,修改一下查询的语句,用Include把Player关联的Resume查处来,因为使用了Include,Player和Resume在内存里存在,再修改就不会有错误了。

using var context = new DemoDbContext();

var player = context.Players
    .Include(x => x.Resume)
    .OrderBy(x => x.Id)
    .LastOrDefault();

player.Resume = new Resume
{
    Description = "12121212121212"
};

context.SaveChanges();

EntityFramework Core进行关系数据的增删改查_第9张图片

可以看到先查询出Player和Resume来,因为要更换新Resume,老的Resume不再依赖Player,也就不应该再存在,就先删掉老的Resume,再Insert新的Resume。如果不写Include,也会报错。

你可能感兴趣的:(EntityFramework Core进行关系数据的增删改查)