我可以使用Entity Framework Version 6或7自动更新对象及其子对象吗?

时间:2016-04-27 01:26:32

标签: c# asp.net entity-framework

我有三张桌子。单词 - > WordForm - > SampleSentence。每个Word都有不同的WordForms,然后每个表单可以包含一个或多个SampleSentence

CREATE TABLE [dbo].[Word] (
    [WordId]       VARCHAR (20) NOT NULL,
    [CategoryId]   INT          DEFAULT ((1)) NOT NULL,
    [GroupId]      INT          DEFAULT ((1)) NOT NULL,
    PRIMARY KEY CLUSTERED ([WordId] ASC),
    CONSTRAINT [FK_WordWordCategory] FOREIGN KEY ([CategoryId]) REFERENCES [dbo].[WordCategory] ([WordCategoryId]),
    CONSTRAINT [FK_WordWordGroup] FOREIGN KEY ([GroupId]) REFERENCES [dbo].[WordGroup] ([WordGroupId])
);

CREATE TABLE [dbo].[WordForm] (
    [WordFormId]   VARCHAR (20)  NOT NULL,
    [WordId]       VARCHAR (20)  NOT NULL,
    [Primary]      BIT           DEFAULT ((0)) NOT NULL,
    [PosId]        INT           NOT NULL,
    [Definition]   VARCHAR (MAX) NULL,
    PRIMARY KEY CLUSTERED ([WordFormId] ASC),
    CONSTRAINT [FK_WordFormPos] FOREIGN KEY ([PosId]) REFERENCES [dbo].[Pos] ([PosId]),
    CONSTRAINT [FK_WordFormWord] FOREIGN KEY ([WordId]) REFERENCES [dbo].[Word] ([WordId])
);


CREATE TABLE [dbo].[SampleSentence] (
    [SampleSentenceId] INT           IDENTITY (1, 1) NOT NULL,
    [WordFormId]       VARCHAR (20)  NOT NULL,
    [Text]             VARCHAR (MAX) NOT NULL,
    CONSTRAINT [PK_SampleSentence] PRIMARY KEY CLUSTERED ([SampleSentenceId] ASC),
    CONSTRAINT [FK_SampleSentenceWordForm] FOREIGN KEY ([WordFormId]) REFERENCES [dbo].[WordForm] ([WordFormId])
);

我将这些表中的数据传输到前端客户端,然后修改数据并添加或删除WordForms和SampleSentences。

然后我将数据带回服务器。

是否存在某种方式,Entity Framework可以检查以查看我带回服务器并对数据库进行更改的对象中的更改,或者我必须进行某种形式的比较,其中我检查了之前和之后的Word,WordForm和Sample Sentence对象?

此处参考的是我使用的C#对象:

public class Word
    {
        public string WordId { get; set; } // WordId (Primary key) (length: 20)
        public int CategoryId { get; set; } // CategoryId
        public int GroupId { get; set; } // GroupId

        // Reverse navigation
        public virtual System.Collections.Generic.ICollection<WordForm> WordForms { get; set; } // WordForm.FK_WordFormWord

        // Foreign keys
        public virtual WordCategory WordCategory { get; set; } // FK_WordWordCategory
        public virtual WordGroup WordGroup { get; set; } // FK_WordWordGroup

        public Word()
        {
            CategoryId = 1;
            GroupId = 1;
            WordForms = new System.Collections.Generic.List<WordForm>();
        }
    }

public class WordForm
    {
        public string WordFormId { get; set; } // WordFormId (Primary key) (length: 20)
        public string WordId { get; set; } // WordId (length: 20)
        public bool Primary { get; set; } // Primary
        public int PosId { get; set; } // PosId
        public string Definition { get; set; } // Definition

        // Reverse navigation
        public virtual System.Collections.Generic.ICollection<SampleSentence> SampleSentences { get; set; } // SampleSentence.FK_SampleSentenceWordForm

        // Foreign keys
        public virtual Pos Pos { get; set; } // FK_WordFormPos
        public virtual Word Word { get; set; } // FK_WordFormWord

        public WordForm()
        {
            Primary = false;
            SampleSentences = new System.Collections.Generic.List<SampleSentence>();
        }
    }

public class SampleSentence : AuditableTable
    {
        public int SampleSentenceId { get; set; } // SampleSentenceId (Primary key)
        public string WordFormId { get; set; } // WordFormId (length: 20)
        public string Text { get; set; } // Text

        // Foreign keys
        public virtual WordForm WordForm { get; set; } // FK_SampleSentenceWordForm
    }

到目前为止,这是我能够提出的,但这不包括检查SampleSentence,我不知道该怎么做:

    public async Task<IHttpActionResult> Put([FromBody]Word word)
    {
        var oldObj = db.WordForms
            .Where(w => w.WordId == word.WordId)
            .AsNoTracking()
            .ToList();
        var newObj = word.WordForms.ToList();

        var upd = newObj.Where(n => oldObj.Any(o =>
            (o.WordFormId == n.WordFormId) && (o.PosId != n.PosId || !o.Definition.Equals(n.Definition) )))
            .ToList();
        var add = newObj.Where(n => oldObj.All(o => o.WordFormId != n.WordFormId))
            .ToList();
        var del = oldObj.Where(o => newObj.All(n => n.WordFormId != o.WordFormId))
            .ToList();
        foreach (var wordForm in upd)
        {
            db.WordForms.Attach(wordForm);
            db.Entry(wordForm).State = EntityState.Modified;
        }
        foreach (var wordForm in add)
        {
            db.WordForms.Add(wordForm);
        }
        foreach (var wordForm in del)
        {
            db.WordForms.Attach(wordForm);
            db.WordForms.Remove(wordForm);
        }
        db.Words.Attach(word);
        db.Entry(word).State = EntityState.Modified;
        await db.SaveChangesAsync(User, DateTime.UtcNow);
        return Ok(word);
    }

4 个答案:

答案 0 :(得分:18)

抱歉,没有

您的问题的答案字面上(如标题中所示)是。无法使用Entity Framework 自动执行此操作。在所谓的断开连接的场景中,正确保存客户端的更改是开发人员应该自己处理的事情。

如前所述,EF过去常常使用自我跟踪实体,但很快这种方法就是deprecated,尽管在官方文档中从未明确说明原因。 可能因为“STEs made (change tracking) easier, but at the cost of making almost everything else really hard.。”它完全适合ObjectContext API与数据库优先生成的类模型和t4模板,但是,众所周知,DbContext API和代码优先已成为EF的推荐(很快就会成为EF)支持)架构。当然,以代码优先,EF不能强制执行任何STE实现。

还是...?

有点令人沮丧的是EF以后从未填补这个空白,例如提供类似于GraphDiff提供的API(或者现在我应该说提供)。我知道有两种合理的替代方案。

实体框架的主张

Lerman和Miller,在他们的书 Programming Entity Framework中: DbContext ,提出了一种替代技术,它最接近EF团队迄今为止提出的自我跟踪实体的替代品。它围绕这个界面:

public interface IObjectWithState
{
    State State { get; set; }
    Dictionary<string, object> OriginalValues { get; set; }
}

State

的位置
public enum State
{
    Added,
    Unchanged,
    Modified,
    Deleted
}

要使此方法正常工作,每个实体都应实现该接口。此外,每个DbContext子类都需要许多方法。实体实现时填充OriginalValues属性的方法,以及将更改跟踪器与实体返回上下文时记录的更改同步的方法。这里复制所有这些代码太多了,你可以在书中找到它,从第102页开始。

好吧,如果你实现了所有这些,你就有了自我跟踪实体。它非常复杂,虽然一旦实施,它“只是工作”。但是,主要缺点是,在添加或删除实体时,上下文的所有使用者都必须设置此State属性。强加客户代码是一项艰巨的任务!

<强>微风

Breeze提供了一个完整的解决方案,从服务中的DAL到客户端中的javascript代码。这既令人难以置信又非常可怕。

在javascript中,你会得到类似LINQ的语法:

var query = breeze.EntityQuery
           .from("Customers")
           .where("CompanyName", "startsWith", "A")
           .orderBy("CompanyName");

这与Creeze代码中的Breeze EntityManager进行通信:

var manager = new Breeze.Sharp.EntityManager(serviceName);
var results = await manager.ExecuteQuery(query);

这个EntityManager基本上是EF上下文的包装器。如果所有移动部件都已正确设置,它实际上会将EF上下文带入您的javascript,并进行更改跟踪,保存更改等等。我在一个项目中使用它,真的,非常方便。

但是如果你使用Breeze,它一直都是Breeze。它会影响一切。数据库架构的更改需要更改javascript。那可怕,但你可以习惯的东西。但是如果你想以自己的方式做事,那么将Breeze根据你的需求变得非常困难(尽管不是不可能)。喜欢和你的婆婆住在一起。我认为在很多情况下,最终Breeze和其他模式的结合变得不可避免。

但是你还想要吗?

一般来说,任何自动跟踪断开连接的实体的一个主要缺点是它使得使用原始实体对象进行数据传输变得非常容易。问题是,在大多数情况下,完整实体包含的数据远远多于客户端要求的数据(或允许查看)。使用专用的纤薄DTO可以显着提高性能。当然,它们充当DAL和UI /控制器之间的抽象层。

是的,对于DTO,我们总是必须“重新绘制状态”服务器端。就这样吧。对于断开连接的场景,它确实是推荐的方法。

在解释Breeze时,John Papa在关于SPA的热毛巾模板的PluralSight课程中认识到了这个问题。他提出了一个“部分实体”的解决方案。 是一个解决方案,但相当复杂和笨重。当然,实体仍然是数据传输的基础。

答案 1 :(得分:5)

正如前面一张海报中所提到的,这在EF中并不是本机支持的,并且是最受欢迎的功能之一。

但是,如果您愿意使用GraphDiff库(或者自己编写类似的东西),则有可能。我强烈建议您使用开源GraphDiff库对EntityFramework中断开连接的图表进行更新。

请查看Brent McKendrick撰写的this article,了解如何执行此操作。

此项目的github存储库也可以找到here

从上面的文章中,更新相关实体的图表就像下面的例子一样简单:

using (var context = new TestDbContext())  
{
    // Update the company and state that the company 'owns' the collection contacts, with contacts having associated advertisement options.
    context.UpdateGraph(company, map => map
        .OwnedCollection(p => p.Contacts, with => with
            .AssociatedCollection(p => p.AdvertisementOptions))
        .OwnedCollection(p => p.Addresses)
    );

    context.SaveChanges();
}

请注意,此项目不再维护,但我已经使用过(以及许多其他项目)而没有任何严重问题。

答案 2 :(得分:1)

查看以下主题:Entity Framework 5 Updating a RecordUpdate relationships when saving changes of EF4 POCO objects。我觉得它很好地描述了所有可能性及其优缺点。基本上每个跟踪系统都会强制您存储原始实体的某个位置以进行比较或再次查询它。提到的自我跟踪实体也是这样做的 - 你必须将原始对象存储在会话或视图状态,这会降低页面的性能(更多关于它的信息:Self Tracking Entities vs POCO Entities)。至于我,我只是将对象的状态更改为修改并让ef更新数据库(正如您现在在For Each r In Range("B2:B" & Cells(Rows.Count, "B").End(xlUp).Row) 方法中所做的那样),这不仅仅是我的观点 - Entity Framework: Update related entities

很多人都要求这样的合并功能(http://entityframework.codeplex.com/workitem/864),但目前EF并没有提供任何支持。但是,一些开发人员注意到有一些像NHibernate这样具有内置功能的替代方案 - http://www.ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html

答案 3 :(得分:1)

是的,您可以,使用以下影响力低下的库来满足您的需求:

https://github.com/AutoMapper/AutoMapper https://github.com/TylerCarlson1/Automapper.Collection

由于各种原因,我通常会将所涉及的类的简化形式传递给客户端(仅包含一组简化的列/属性)。

请注意,EF类位于ORM名称空间中,而简化类位于当前名称空间中。

您可以在下面找到一些示例代码:

private IMapper map;

private void InitMapper()
{
    map = new MapperConfiguration(cfg => {
        cfg.CreateMap<ORM.SampleSentence, SampleSentence>();
        cfg.CreateMap<ORM.WordForm, WordForm>();
        cfg.CreateMap<ORM.Word, Word>();
        cfg.CreateMap<SampleSentence, ORM.SampleSentence>();
        cfg.CreateMap<WordForm, ORM.WordForm>();
        cfg.CreateMap<Word, ORM.Word>();
        cfg.AddProfile<CollectionProfile>();
    }).CreateMapper();

    EquivilentExpressions.GenerateEquality.Add(new GenerateEntityFrameworkPrimaryKeyEquivilentExpressions<YourDbContext>(map));
}

public async Task<IHttpActionResult> Put([FromBody]Word word)
{
    var dbWord = db.Word.Include(w => w.WordForm).Where(w => w.WordId == word.WordId).First();

    if (dbWord != null)
    {
        InitMapper();
        map.Map(word, dbWord);
        db.SaveChanges();

        var ret = map.Map<Word>(dbWord);

        return Ok(ret);
    }
    else
    {
        return NotFound();
    }

}

此解决方案运行良好,EF将发布仅包含实际更改的字段的更新。

注意:使用AutoMapper,您还可以配置哪些字段必须更新/映射(每个方向也不同)。示例:您希望某些字段对于用户来说是“只读”,您可以使用 opt.Ignore()在Object-&gt; ORM.Object方向中标记 p>

    cfg.CreateMap<Word, ORM.Word>()
        .ForMember(dest => dest.WordId, opt => opt.Ignore())