【问题标题】:Can I use Entity Framework Version 6 or 7 to update an object and its children automatically?我可以使用实体框架版本 6 或 7 自动更新对象及其子对象吗?
【发布时间】:2016-04-27 01:26:32
【问题描述】:

我有三张桌子。 Word -> 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。

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

实体框架是否可以通过某种方式检查我带回服务器并对数据库进行更改的对象的更改,或者我是否必须进行某种形式的比较,以检查之前和之后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);
    }

【问题讨论】:

  • 相关:stackoverflow.com/questions/9503286/…。无论如何,保持现有记录很重要吗?否则,您可以删除 Word 并简单地重新插入已发布的 Word
  • 艾伦,如果您可以使用 Jquery 检测更改并维护一个标志,您可以稍后将其发送到服务器端,然后决定是否要更新表,那么您可以减轻负担
  • 自我跟踪实体怎么样?是的,不再推荐它们,但这并不意味着它们不能解决您的问题。
  • @CodeCaster - 我会考虑你的建议。这听起来像一个好主意。虽然在服务器端,我认为我需要有一种方法来识别所有新的 WordForm 和 SampleSentence,以便 EF 知道添加这些。
  • @Alan 您能解决您的问题吗?您是如何解决的?

标签: c# asp.net entity-framework


【解决方案1】:

对不起,没有

您的问题字面意思(如标题)的答案是。没有办法使用实体框架自动做到这一点。在所谓的断开连接的场景中,正确保存来自客户端的更改是开发人员应该自己处理的事情。

如前所述,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 无法强制执行任何 STE 实现。

还是……?

有些令人沮丧的是,EF 以后从未填补这一空白,例如通过提供类似于 GraphDiff 提供的 API(或者现在我应该说 提供)。我知道有两种合理的选择。

Entity Framework 的命题

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");

这与 Breeze 在 C# 代码中的 EntityManager 通信:

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

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

但是,如果您使用 Breeze,那么它就是 Breeze。它影响一切。更改数据库架构需要更改 javascript。这很可怕,但你可以习惯的。但是,如果您想以自己的方式做事,则很难(尽管并非不可能)使 Breeze 满足您的需求。就像和婆婆住在一起一样。我认为在很多情况下,Breeze 和其他模式的结合最终变得不可避免。

但是你还是想要它吗?

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

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

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

【讨论】:

  • ...让 Breeze 满足您的需求变得非常困难(尽管并非不可能)。就像和你岳母住在一起一样。 Bravo Sir。
【解决方案2】:

正如之前的一张海报所提到的,这在 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();
}

请注意,此项目不再维护,尽管我使用它(以及许多其他项目)没有任何严重问题。

【讨论】:

  • 我想补充一点,作为个人喜好,EF 不太适合复杂的数据库写入操作。更好的方法是使用 T-SQL 存储过程进行复杂的数据库写入操作。
【解决方案3】:

是的,您可以,通过使用以下满足您需要的低影响库:

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,您还可以配置必须更新/映射的字段(每个方向也不同)。示例:您希望某些字段对用户“只读”,您可以在 Object->ORM.Object 方向用 opt.Ignore() 标记它们

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

【讨论】:

    【解决方案4】:

    查看以下主题:Entity Framework 5 Updating a RecordUpdate relationships when saving changes of EF4 POCO objects。我觉得它很好地描述了所有可能性及其优缺点。基本上每个跟踪系统都会强制您将原始实体存储在某处以进行比较或再次查询。提到的自我跟踪实体也是如此 - 您必须将原始对象存储在会话或视图状态中,这会降低页面的性能(这里有更多关于它的信息:Self Tracking Entities vs POCO Entities)。至于我,我只需将对象的状态更改为 modified 并让 ef 更新数据库(就像您现在在 Put([FromBody]Word word) 方法中所做的那样),这不仅是我的观点 - 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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多