0

我正在编写一个 Web 应用程序,这样我就可以从 Web 获取不同的对象,这些对象需要更新或添加到数据库中。最重要的是,我需要检查所有者是否没有被修改。因为黑客可能会获得一个帐户并发送更新以修改用户模型的外键。我不想手动编写所有这些方法,而是想进行一个简单的通用调用。

也许像这样简单的事情

ctx.OrderLines.AddOrUpdateSet(order.OrderLines, a => a.Order)

基于具有 Order 外键的旧持久记录,以及新的传入记录。

  1. 删除不在新记录列表中的旧记录。

  2. 添加不在旧记录列表中的新记录。

  3. 更新两个列表中存在的新记录。

    ctx.Entry(orderLine).State=EntityState.Deleted;
               ...
    ctx.Entry(orderLine).State=EntityState.Added;
               ...
    ctx.Entry(orderLine).State=EntityState.Modified;
    

当加载旧记录以验证所有权没有改变时,这会变得有点复杂。如果我不这样做,我会收到错误消息。

   oldorder.OrderLines.remove(oldOrderLine); //for deletes
   oldorder.OrderLines.add(oldOrderLine); //for adds
   ctx.Entry(header).CurrentValues.SetValues(header); //for modifications

在 Entity Framework 5 中,有一个名为 AddOrUpdate 的新扩展函数。还有一个非常有趣的(请阅读)博客条目,介绍了如何在添加此方法之前创建它。

我不确定这作为 StackOverflow 中的一个问题是否过多,任何关于如何解决问题的线索可能就足够了。到目前为止,这是我的想法:

a) 将 AddOrUpdate 用于某些功能。

b)创建辅助上下文,希望避免将订单加载到上下文中并避免额外的调用。

c) 将所有已保存对象的状态设置为初始删除。

4

3 回答 3

2

既然你已经从我自己的问题中链接到这个问题,我想我会为我提供一些新获得的 Entity Framework 经验。

为了在我的通用存储库中使用 Entity Framework 实现一个通用的保存方法,我这样做了。(请注意,上下文是我的存储库的成员,因为我也在实现工作单元模式)

public class EFRepository<TEntity> : IRepository<TEntity> where TEntity : class
{
    internal readonly AwesomeContext Context;
    internal readonly DbSet<TEntity> DbSet;

    public EFRepository(AwesomeContext context)
    {
        if (context == null) throw new ArgumentNullException("context");

        Context = context;
        DbSet = context.Set<TEntity>();
    }

    // Rest of implementation removed for brevity 

    public void Save(TEntity entity)
    {
        var entry = Context.Entry(entity);
        if (entry.State == EntityState.Detached)
            DbSet.Add(entity);
        else entry.State = EntityState.Modified;
    }
}

老实说,我无法告诉你为什么会这样,因为我只是不断地改变状态条件——但是我确实有单元(集成)测试来证明它是有效的。希望比我更喜欢 EF 的人能对此有所了解。

关于“级联更新”,我很好奇自己是否可以使用工作单元模式(我链接到的问题是当我不知道它存在时,我的存储库基本上会在我想要的时候创建一个工作单元保存/获取/删除,这很糟糕),所以我在一个简单的关系数据库中放入了一个测试用例。这是一个图表,可以给你一个想法。

模型关系图

重要为了使测试用例 2 工作,您需要使您的 POCO 引用属性虚拟化,以便 EF 提供延迟加载。

EFRepository<TEntity>如上所示,存储库实现只是从泛型派生的,因此我将省略该实现。

这些是我的测试用例,都通过了。

public class EFResourceGroupFacts
{
    [Fact]
    public void Saving_new_resource_will_cascade_properly()
    {
        // Recreate a fresh database and add some dummy data.
        SetupTestCase();

        using (var ctx = new LocalizationContext("Localization.CascadeTest"))
        {
            var cultureRepo = new EFCultureRepository(ctx);
            var resourceRepo = new EFResourceRepository(cultureRepo, ctx);

            var existingCulture = cultureRepo.Get(1); // First and only culture.
            var groupToAdd = new ResourceGroup("Added Group");
            var resourceToAdd = new Resource(existingCulture,"New Resource", "Resource to add to existing group.",groupToAdd);

            // Verify we got a single resource group.
            Assert.Equal(1,ctx.ResourceGroups.Count());

            // Saving the resource should also add the group.
            resourceRepo.Save(resourceToAdd);
            ctx.SaveChanges();

            // Verify the group was added without explicitly saving it.
            Assert.Equal(2, ctx.ResourceGroups.Count());
        }

        // try creating a new Unit of Work to really verify it has been persisted..
        using (var ctx = new LocalizationContext("Localization.CascadeTest"))
        {
            Assert.DoesNotThrow(() => ctx.ResourceGroups.First(rg => rg.Name == "Added Group"));
        }
    }

    [Fact]
    public void Changing_existing_resources_group_saves_properly()
    {
        SetupTestCase();

        using (var ctx = new LocalizationContext("Localization.CascadeTest"))
        {
            ctx.Configuration.LazyLoadingEnabled = true;
            var cultureRepo = new EFCultureRepository(ctx);
            var resourceRepo = new EFResourceRepository(cultureRepo, ctx);



            // This resource already has a group.
            var existingResource = resourceRepo.Get(2);
            Assert.NotNull(existingResource.ResourceGroup); // IMPORTANT: Property must be virtual!
            // Verify there is only one resource group in the datastore.
            Assert.Equal(1,ctx.ResourceGroups.Count());

            existingResource.ResourceGroup = new ResourceGroup("I am implicitly added to the database. How cool is that?");

            // Make sure there are 2 resources in the datastore before saving.
            Assert.Equal(2, ctx.Resources.Count());

            resourceRepo.Save(existingResource);
            ctx.SaveChanges();

            // Make sure there are STILL only 2 resources in the datastore AFTER saving.
            Assert.Equal(2, ctx.Resources.Count());

            // Make sure the new group was added.
            Assert.Equal(2,ctx.ResourceGroups.Count());

            // Refetch from store, verify relationship.
            existingResource = resourceRepo.Get(2);
            Assert.Equal(2,existingResource.ResourceGroup.Id);

             // let's change the group to an existing group
            existingResource.ResourceGroup = ctx.ResourceGroups.First();
            resourceRepo.Save(existingResource);
            ctx.SaveChanges();

            // Assert no change in groups.
            Assert.Equal(2, ctx.ResourceGroups.Count());

            // Refetch from store, verify relationship.
            existingResource = resourceRepo.Get(2);
            Assert.Equal(1, existingResource.ResourceGroup.Id);
        }
    }

    private void SetupTestCase()
    {
        // Delete everything first. Database.SetInitializer does not work very well for me.
        using (var ctx = new LocalizationContext("Localization.CascadeTest"))
        {
            ctx.Database.Delete();
            ctx.Database.Create();

            var culture = new Culture("en-US", "English");
            var resourceGroup = new ResourceGroup("Existing Group");
            var resource = new Resource(culture, "Existing Resource 1",
                                        "This resource will already exist when starting the test. Initially it has no group.");
            var resourceWithGroup = new Resource(culture, "Exising Resource 2",
                                                 "Same for this resource, except it has a group.",resourceGroup);

            ctx.Cultures.Add(culture);
            ctx.ResourceGroups.Add(resourceGroup);
            ctx.Resources.Add(resource);
            ctx.Resources.Add(resourceWithGroup);

            ctx.SaveChanges();
        }
    }
}

学习这个很有趣,因为我不确定它是否会起作用。

于 2013-07-12T09:07:32.330 回答
2

在研究了一段时间后,我发现了一个名为 GraphDiff 的开源项目,这是它的博客条目“首先为实体框架代码引入 graphdiff - 允许自动更新分离实体的图形”。我才开始使用它,但它看起来令人印象深刻。它确实解决了为多对一关系发布更新/删除/插入的问题。它实际上将问题推广到图形并允许任意嵌套。

于 2013-07-17T18:59:53.070 回答
0

这是我炮制的通用方法。它确实使用了 System.Data.Entity.Migrations 命名空间中的 AddOrUpdate。这可能是从数据库重新加载记录,我稍后会检查。用法是

ctx.OrderLines.AddOrUpdateSet(l => l.orderId == neworder.Id, 
    l => l.Id, order.orderLines);

这是代码:

public static class UpdateExtensions
{
    public static void AddOrUpdateSet<TEntity>(this IDbSet<TEntity> set, Expression<Func<TEntity, bool>> predicate,
        Func<TEntity, int> selector, IEnumerable<TEntity> newRecords) where TEntity : class
    {
        List<TEntity> oldRecords = set.Where(predicate).ToList();
        IEnumerable<int> keys = newRecords.Select(selector);

        foreach (TEntity newRec in newRecords) 
            set.AddOrUpdate(newRec);

        oldRecords.FindAll(old => !keys.Contains(selector(old))).ForEach(detail => set.Remove(detail));
    }
}
于 2013-07-13T03:32:31.963 回答