使用EF自带的EntityState枚举和自定义枚举实现单个和多个实体的增删改查
本文目录
之前使用EF都是通过调用SaveChanges方法把增加/修改/删除的数据提交到数据库,但是上下文是如何知道实体对象是增加、修改还是删除呢?答案是通过EntityState枚举来判断的,看一个方法:
/// <summary> /// 查看实体状态 /// </summary> private static void GetOneEntityToSeeEntityState() { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { var destination = context.Destinations.Find(4); EntityState stateBefore = context.Entry(destination).State; Console.WriteLine(stateBefore); } }
注:使用EntityState需添加引用system.data
跑下程序,输出结果为:Unchanged。从英文意思便可以猜到一二:取出来的数据是Unchanged,那么添加、修改、删除自然也就是Added、Modified、Deleted了。在EntityState上按F12定位到其定义看看:
的确,当调用SaveChanges方法的时候,EF会根据EntityState这个枚举检测到实体的状态,然后执行相应的增/删/改操作。它们的具体意思分别为:
- Detached:对象存在,但未由对象服务跟踪。在创建实体之后、但将其添加到对象上下文之前,该实体处于此状态;
- Unchanged:自对象加载到上下文中后,或自上次调用 System.Data.Objects.ObjectContext.SaveChanges() 方法后,此对象尚未经过修改;
- Added:对象已添加到对象上下文,但尚未调用 System.Data.Objects.ObjectContext.SaveChanges() 方法;
- Deleted:使用 System.Data.Objects.ObjectContext.DeleteObject(System.Object) 方法从对象上下文中删除了对象;
- Modified:对象已更改,但尚未调用 System.Data.Objects.ObjectContext.SaveChanges() 方法。
ok,已经知道了这个,利用EntityState这个枚举修改下之前单个实体的增删改方法:
增加:
/// <summary> /// 添加:DbSet.Add = > EntityState.Added /// </summary> private static void TestAddDestination() { var jacksonHole = new DbContexts.Model.Destination { Name = "Jackson Hole,Wyoming", Description = "Get your skis on." }; //AddDestinationByDbSetAdd(jacksonHole); AddDestinationByEntityStateAdded(jacksonHole); } private static void AddDestinationByDbSetAdd(DbContexts.Model.Destination destination) { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { context.Destinations.Add(destination); context.SaveChanges(); } } private static void AddDestinationByEntityStateAdded(DbContexts.Model.Destination destination) { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { Console.WriteLine(context.Entry(destination).State); //添加前:Detached context.Entry(destination).State = EntityState.Added; Console.WriteLine(context.Entry(destination).State); //添加后:Added context.SaveChanges(); } }
exec sp_executesql N'insert [baga].[Locations]([LocationName], [Country], [Description], [Photo], [TravelWarnings], [ClimateInfo]) values (@0, null, @1, null, null, null) select [LocationID] from [baga].[Locations] where @@ROWCOUNT > 0 and [LocationID] = scope_identity()',N'@0 nvarchar(200),@1 nvarchar(500)',@0=N'Jackson Hole,Wyoming',@1=N'Get your skis on.'
监控到的sql也跟之前调用DbSet.Add方法添加实体的没什么区别。
注:上面的两个添加方法都是通过传递对象的形式,并没有在using语句块里初始化对象,这也更符合实际项目分层之间传递对象的情况。AddDestinationByEntityStateAdded方法里标记添加前对象的状态是Detached,即未被上下文追踪到,这个对象是别的方法传递进来的,当然不会被这个using语句块里的上下文追踪到。如果在上下文里取对象,状态就是Unchanged,即未改变的意思。感谢wlf在评论里的提醒
继续看一个标记实体为未改变实体:
/// <summary> /// 标记一个未改变的实体 /// </summary> private static void TestAttachDestination() { DbContexts.Model.Destination canyon; using (var context = new DbContexts.DataAccess.BreakAwayContext()) { canyon = (from d in context.Destinations where d.Name == "Grand Canyon" select d).Single(); } AttachDestination(canyon); } private static void AttachDestination(DbContexts.Model.Destination destination) { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { Console.WriteLine(context.Entry(destination).State); //标记前:Detached context.Destinations.Attach(destination); //修改使用Attach方法 //context.Entry(destination).State = EntityState.Unchanged; //跟Attach方法一样效果 Console.WriteLine(context.Entry(destination).State); //标记后:Unchanged context.SaveChanges(); } }
调用Attach方法变成了Unchanged状态。Unchanged状态会被SaveChanges方法忽略掉,不会有任何sql发送到数据库。
修改:
/// <summary> /// 修改:EntityState.Modified /// </summary> private static void TestUpdateDestination() { DbContexts.Model.Destination canyon; using (var context = new DbContexts.DataAccess.BreakAwayContext()) { canyon = (from d in context.Destinations where d.Name == "Grand Canyon" select d).Single(); } canyon.TravelWarnings = "Don't Fall in!"; UpdateDestination(canyon); } private static void UpdateDestination(DbContexts.Model.Destination destination) { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { Console.WriteLine(context.Entry(destination).State); //修改前:Detached context.Entry(destination).State = EntityState.Modified; Console.WriteLine(context.Entry(destination).State); //修改后:Modified context.SaveChanges(); } }
exec sp_executesql N'update [baga].[Locations] set [LocationName] = @0, [Country] = @1, [Description] = @2, [Photo] = null, [TravelWarnings] = @3, [ClimateInfo] = null where ([LocationID] = @4) ',N'@0 nvarchar(200),@1 nvarchar(max) ,@2 nvarchar(500),@3 nvarchar(max) ,@4 int',@0=N'Grand Canyon',@1=N'USA',@2=N'One huge canyon.',@3=N'Don''t Fall in!',@4=1
标记实体为Modified后调用SaveChanges方法后,EF知道要更新实体了,但是它并不知道具体更新的是哪一列,所以每一列都更新了。见上面的sql
删除:
/// <summary> /// 删除:DbSet.Remove = > EntityState.Deleted /// </summary> private static void TestDeleteDestination() { DbContexts.Model.Destination canyon; using (var context = new DbContexts.DataAccess.BreakAwayContext()) { canyon = (from d in context.Destinations where d.Name == "Grand Canyon" select d).Single(); } //DeleteDestination(canyon); DeleteDestinationByEntityStateDeletion(canyon); } private static void DeleteDestination(DbContexts.Model.Destination destination) { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { context.Destinations.Attach(destination); //先告诉EF这个实体,状态变化:Detached -> Unchanged context.Destinations.Remove(destination); //执行删除 context.SaveChanges(); } } private static void DeleteDestinationByEntityStateDeletion(DbContexts.Model.Destination destination) { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { Console.WriteLine(context.Entry(destination).State); //删除前:Detached context.Entry(destination).State = EntityState.Deleted; Console.WriteLine(context.Entry(destination).State); //删除后:Deleted context.SaveChanges(); } }
exec sp_executesql N'delete [baga].[Locations] where ([LocationID] = @0)',N'@0 int',@0=1
添加主从表数据(主外键关系实体间的递归):
/// <summary> /// 添加一组数据(主从表) /// </summary> private static void AddSimpleGraph() { var essex = new DbContexts.Model.Destination { Name = "Essex, Vermont", Lodgings = new List<DbContexts.Model.Lodging> { new DbContexts.Model.Lodging{Name="Big Essex Hotel"}, new DbContexts.Model.Lodging{Name="Essex Junction B&B"} } }; using (var context = new DbContexts.DataAccess.BreakAwayContext()) { context.Destinations.Add(essex); Console.WriteLine("Essex Destination:{0}", context.Entry(essex).State); foreach (var lodging in essex.Lodgings) { Console.WriteLine("{0}:{1}", lodging.Name, context.Entry(lodging).State); } context.SaveChanges(); } }
上面的方法中只有主表Destination的数据调用了上下文的Add方法。调用上下文的Add方法的实体就会被上下文跟踪到,执行SaveChanges方法时如果实体的EntityState是Added,那么就会被插入到数据库。但是从表为何也被标记为Add了呢?因为EF根据主表查询从表的过程是递归(recursive)的。如图:
当然在调用方法时也得注意,否则实体的状态会被修改,调用SaveChanges方法的时候执行的sql也就不同了。看看这个图就明白了:
以上的方法都是在一个上下文对象里操作的,但是在真实的项目中用户在客户端对实体做完各种增删改操作后提交服务端这边,上下文对象肯定是不一样的。在客户端被标记为不同状态的实体(Added、Deteled、Modified等)到服务端的上下文就不认识这些实体了,那如何辨别实体的各种状态呢?继续往下看:
/// <summary> /// 复杂的添加和删除 /// </summary> private static void TestSaveDestinationAndLodgings() { DbContexts.Model.Destination canyon; using (var context = new DbContexts.DataAccess.BreakAwayContext()) { canyon = (from d in context.Destinations.Include(d => d.Lodgings) where d.Name == "Grand Canyon" select d).Single(); }//查询【第一段sql】 canyon.TravelWarnings = "Carry enough water!";//修改【第二段sql】 canyon.Lodgings.Add(new DbContexts.Model.Lodging { Name = "Big Canyon Lodge" });//增加【第三段sql】 var firstLodging = canyon.Lodgings.ElementAt(0); firstLodging.Name = "New Name Holiday Park";//修改【第四段sql】 var secondLodging = canyon.Lodgings.ElementAt(1); var deletedLodgings = new List<DbContexts.Model.Lodging>(); canyon.Lodgings.Remove(secondLodging);//删除【第五段sql】 deletedLodgings.Add(secondLodging); SaveDestinationAndLodgings(canyon, deletedLodgings); }
增加/删除/修改等操作都不在了using语句块里了,很明显就不能直接调用SaveChanges方法提交到数据库了。看下面方法如何还原实体状态并正确的提交数据库:
/// <summary> /// 设置每一个实体状态保证正确的提交修改 /// </summary> /// <param name="destination">要添加的实体</param> /// <param name="deteledLodgings">要删除的实体</param> private static void SaveDestinationAndLodgings(DbContexts.Model.Destination destination, List<DbContexts.Model.Lodging> deteledLodgings) { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { context.Destinations.Add(destination); if (destination.DestinationId > 0) //避免了新添加的实体 context.Entry(destination).State = EntityState.Modified; foreach (var lodging in destination.Lodgings) { if (lodging.LodgingId > 0) context.Entry(lodging).State = EntityState.Modified; } foreach (var lodging in deteledLodgings) { context.Entry(lodging).State = EntityState.Deleted; } context.SaveChanges(); } }
为何上面的方法都是用DestinationId > 0来当作条件呢?因为主键id是自增长的,大于0说明是已经存在的记录,那么自然是修改。新添加的DestinationId默认都是0。看看生成的sql吧:
【第一段sql】贪婪加载
SELECT [Project1].[LocationID] AS [LocationID], [Project1].[LocationName] AS [LocationName], [Project1].[Country] AS [Country], [Project1].[Description] AS [Description], [Project1].[Photo] AS [Photo], [Project1].[TravelWarnings] AS [TravelWarnings], [Project1].[ClimateInfo] AS [ClimateInfo], [Project1].[C1] AS [C1], [Project1].[Discriminator] AS [Discriminator], [Project1].[LodgingId] AS [LodgingId], [Project1].[Name] AS [Name], [Project1].[Owner] AS [Owner], [Project1].[MilesFromNearestAirport] AS [MilesFromNearestAirport], [Project1].[destination_id] AS [destination_id], [Project1].[PrimaryContactId] AS [PrimaryContactId], [Project1].[SecondaryContactId] AS [SecondaryContactId], [Project1].[Entertainment] AS [Entertainment], [Project1].[Activities] AS [Activities], [Project1].[MaxPersonsPerRoom] AS [MaxPersonsPerRoom], [Project1].[PrivateRoomsAvailable] AS [PrivateRoomsAvailable] FROM ( SELECT [Limit1].[LocationID] AS [LocationID], [Limit1].[LocationName] AS [LocationName], [Limit1].[Country] AS [Country], [Limit1].[Description] AS [Description], [Limit1].[Photo] AS [Photo], [Limit1].[TravelWarnings] AS [TravelWarnings], [Limit1].[ClimateInfo] AS [ClimateInfo], [Extent2].[LodgingId] AS [LodgingId], [Extent2].[Name] AS [Name], [Extent2].[Owner] AS [Owner], [Extent2].[MilesFromNearestAirport] AS [MilesFromNearestAirport], [Extent2].[destination_id] AS [destination_id], [Extent2].[PrimaryContactId] AS [PrimaryContactId], [Extent2].[SecondaryContactId] AS [SecondaryContactId], [Extent2].[Entertainment] AS [Entertainment], [Extent2].[Activities] AS [Activities], [Extent2].[MaxPersonsPerRoom] AS [MaxPersonsPerRoom], [Extent2].[PrivateRoomsAvailable] AS [PrivateRoomsAvailable], [Extent2].[Discriminator] AS [Discriminator], CASE WHEN ([Extent2].[LodgingId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1] FROM (SELECT TOP (2) [Extent1].[LocationID] AS [LocationID], [Extent1].[LocationName] AS [LocationName], [Extent1].[Country] AS [Country], [Extent1].[Description] AS [Description], [Extent1].[Photo] AS [Photo], [Extent1].[TravelWarnings] AS [TravelWarnings], [Extent1].[ClimateInfo] AS [ClimateInfo] FROM [baga].[Locations] AS [Extent1] WHERE N'Grand Canyon' = [Extent1].[LocationName] ) AS [Limit1] LEFT OUTER JOIN [dbo].[Lodgings] AS [Extent2] ON ([Extent2].[Discriminator] IN ('Resort','Hostel','Lodging')) AND ([Limit1].[LocationID] = [Extent2].[destination_id]) ) AS [Project1] ORDER BY [Project1].[LocationID] ASC, [Project1].[C1] ASC
【第二段sql】
exec sp_executesql N'update [baga].[Locations] set [LocationName] = @0, [Country] = @1, [Description] = @2, [Photo] = null, [TravelWarnings] = @3, [ClimateInfo] = null where ([LocationID] = @4) ',N'@0 nvarchar(200),@1 nvarchar(max) ,@2 nvarchar(500),@3 nvarchar(max) ,@4 int',@0=N'Grand Canyon',@1=N'USA',@2=N'One huge canyon.',@3=N'Carry enough water!',@4=1
【第三段sql】
exec sp_executesql N'insert [dbo].[Lodgings]([Name], [Owner], [MilesFromNearestAirport], [destination_id], [PrimaryContactId], [SecondaryContactId], [Entertainment], [Activities], [MaxPersonsPerRoom], [PrivateRoomsAvailable], [Discriminator]) values (@0, null, @1, @2, null, null, null, null, null, null, @3) select [LodgingId] from [dbo].[Lodgings] where @@ROWCOUNT > 0 and [LodgingId] = scope_identity()',N'@0 nvarchar(200),@1 decimal(18,2),@2 int,@3 nvarchar(128)',@0=N'Big Canyon Lodge',@1=0,@2=1,@3=N'Lodging'
【第四段sql】
exec sp_executesql N'update [dbo].[Lodgings] set [Name] = @0, [Owner] = null, [MilesFromNearestAirport] = @1, [destination_id] = @2, [PrimaryContactId] = null, [SecondaryContactId] = null where ([LodgingId] = @3) ',N'@0 nvarchar(200),@1 decimal(18,2),@2 int,@3 int',@0=N'New Name Holiday Park',@1=2.50,@2=1,@3=1
【第五段sql】
exec sp_executesql N'delete [dbo].[Lodgings] where ([LodgingId] = @0)',N'@0 int',@0=2
跟踪下上面方法里的实体状态:
/// <summary> /// 设置每一个实体状态保证正确的提交修改 /// </summary> /// <param name="destination">要添加的实体</param> /// <param name="deteledLodgings">要删除的实体</param> private static void SaveDestinationAndLodgings(DbContexts.Model.Destination destination, List<DbContexts.Model.Lodging> deteledLodgings) { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { context.Destinations.Add(destination); if (destination.DestinationId > 0) //避免了新添加的实体 context.Entry(destination).State = EntityState.Modified; foreach (var lodging in destination.Lodgings) { if (lodging.LodgingId > 0) context.Entry(lodging).State = EntityState.Modified; } foreach (var lodging in deteledLodgings) { context.Entry(lodging).State = EntityState.Deleted; } //看看每个实体的状态 Console.WriteLine("{0}:{1}", destination.Name, context.Entry(destination).State); foreach (var lodging in destination.Lodgings) { Console.WriteLine("{0}:{1}", lodging.Name, context.Entry(lodging).State); } foreach (var lodging in deteledLodgings) { Console.WriteLine("{0}:{1}", lodging.Name, context.Entry(lodging).State); } context.SaveChanges(); } }
的确如我们所需:主表数据是修改、从表数据一个修改一个添加一个删除。这样再调用上下文的SaveChanges方法的时候都能正确的提交数据库。看到这里可能疑惑了:这个Big Canyon Lodge的状态为何是Added?因为主表调用了Add方法,相关联的从表实体自动也被标记为Added状态了。上面有提到,EF的这个过程是递归的。
不过此方法弊端也很明显:如果要增/删/改的实体很多,那么还得挨个设置EntityState才可以。试着尝试着让实体实现自定义的IObjectWithState接口,IObjectWithState接口可以记录实体的状态,并且是独立于EF存在的。首先到Model类库上添加一个IObjectWithState接口:
namespace DbContexts.Model { public interface IObjectWithState { State State { get; set; } } public enum State { Added, Unchanged,
Modified, Deleted } }
由于独立于EF而存在,所以从数据库取出来的对象得手动设置为Unchanged状态让EF跟踪到,否则取出来的实体都是Detached状态。最好的做法就是在数据库上下文类里监听ObjectMaterialized事件:
public BreakAwayContext() : base("name=BreakAwayContext") { ((IObjectContextAdapter)this).ObjectContext .ObjectMaterialized += (sender, args) => { var entity = args.Entity as DbContexts.Model.IObjectWithState; if (entity != null) { entity.State = DbContexts.Model.State.Unchanged; } }; }
注:需要引用命名空间:System.Data.Entity.Infrastructure
然后让Destination和Lodging类分别继承IObjectWithState接口:
public class Destination : IObjectWithState public class Lodging : IObjectWithState
都设置好了添加一个方法试试:
/// <summary> /// 设置实体的状态为自定义的枚举值,然后统一跟踪 /// </summary> public static void SaveDestinationGraph(DbContexts.Model.Destination destination) { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { context.Destinations.Add(destination); foreach (var entry in context.ChangeTracker.Entries<DbContexts.Model.IObjectWithState>()) { DbContexts.Model.IObjectWithState stateInfo = entry.Entity; entry.State = ConvertState(stateInfo.State); } context.SaveChanges(); } } public static EntityState ConvertState(DbContexts.Model.State state) { switch (state) { case DbContexts.Model.State.Added: return EntityState.Added;
case DbContexts.Model.State.Modified:
return EntityState.Modified; case DbContexts.Model.State.Deleted: return EntityState.Deleted; default: return EntityState.Unchanged; } }
方法分析:首先是一个DbSet.Add方法标记主表实体的状态为Added,主表数据其实就是根数据(Root Destination)。正如本文前面演示的根数据被标记为Added状态了,那么相关联的从表数据也自动标记为了Added状态。然后使用ChangeTracker.Entries<TEntity>方法查出所有被上下文跟踪到的实体状态,并通过IObjectWithState接口把上下文中保留的实体状态都设置成了自定义的状态State。这样做有什么好处呢?继续向下看
private static void TestSaveDestinationGraph() { DbContexts.Model.Destination canyon; using (var context = new DbContexts.DataAccess.BreakAwayContext()) { canyon = (from d in context.Destinations.Include(d => d.Lodgings) where d.Name == "Grand Canyon" select d).Single(); } canyon.TravelWarnings = "Carry enough water!"; canyon.State = DbContexts.Model.State.Modified; //设置为自定义的枚举,跟EF的EntityState没关系 var firstLodging = canyon.Lodgings.First(); firstLodging.Name = "New Name Holiday Park"; firstLodging.State = DbContexts.Model.State.Modified; //设置为自定义的枚举 var secondLodging = canyon.Lodgings.Last(); secondLodging.State = DbContexts.Model.State.Deleted; //设置为自定义的枚举 canyon.Lodgings.Add(new DbContexts.Model.Lodging { Name = "Big Canyon Lodge", State = DbContexts.Model.State.Added //设置为自定义的枚举 }); SaveDestinationGraph(canyon); }
跟上面的TestSaveDestinationAndLodgings方法基本是类似的,生成的sql也一模一样,不过方法里并没修改每个实体的EntityState,而是设置成了自定义的枚举,好处显而易见:不需要再拿destination.DestinationId > 0 什么来判断哪个实体需要设置成什么状态了。扩展性很强,就算有100个实体被标记为了不同的增删改状态,也不需要挨个判断并设置了。
但是这个还不通用,只能针对demo里的操作,来一个更通用的:
/// <summary> /// 通用的转换实体状态方法 /// </summary> /// <typeparam name="TEntity">要操作的实体</typeparam> /// <param name="root">根实体</param> private static void ApplyChanges<TEntity>(TEntity root) where TEntity : class, DbContexts.Model.IObjectWithState { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { context.Set<TEntity>().Add(root); CheckForEntitiesWithoutStateInterface(context); //检查 foreach (var entry in context.ChangeTracker.Entries<DbContexts.Model.IObjectWithState>()) { DbContexts.Model.IObjectWithState stateInfo = entry.Entity; entry.State = ConvertState(stateInfo.State); } context.SaveChanges(); } } /// <summary> /// 检查实体是否实现了IObjectWithState接口 /// </summary> private static void CheckForEntitiesWithoutStateInterface(DbContexts.DataAccess.BreakAwayContext context) { var entitiesWithoutState = from e in context.ChangeTracker.Entries() where !(e.Entity is DbContexts.Model.IObjectWithState) select e; if (entitiesWithoutState.Any()) throw new NotSupportedException("All entities must implement IObjectWithState"); }
注:需要加上检查实体是否实现了自定义的IObjectWithState接口,否则方法跑完每个实体都被标记为了EntityState.Added状态。
感谢阅读,希望我的分析能给你带来思考并有所进步。本文源码
EF DbContext 系列文章导航: