您的位置:首页 > 其它

使用EF自带的EntityState枚举和自定义枚举实现单个和多个实体的增删改查

2013-09-02 09:49 441 查看
本文目录

使用EntityState枚举实现单个实体的增/删/改

增加:DbSet.Add = > EntityState.Added

标记实体为未改变:EntityState.Unchanged

修改:EntityState.Modified

删除:DbSet.Remove = > EntityState.Deleted

EF里实体状态的递归(recursive)

不被上下文追踪的情况下实现增删改操作

让实体实现自定义的IObjectWithState接口来设置实体状态

通用的转换实体状态方法

本文源码和系列文章目录

之前使用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定位到其定义看看:

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


View Code
【第二段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状态。

感谢阅读,希望我的分析能给你带来思考并有所进步。本文源码

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐