Могу ли я использовать Entity Framework версии 6 или 7 для автоматического обновления объекта и его детей?
У меня три таблицы. 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.
Затем я возвращаю данные на сервер.
Есть ли способ, с помощью которого Entity Framework может проверять изменения в объекте, который я возвращаю на сервер, и вносить изменения в базу данных, или мне нужно сделать некоторую форму сравнения, где я проверяю до и после Word, WordForm и Sample Sentence?
Для справки здесь используются объекты С#:
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);
}
Ответы
Ответ 1
Извините, нет
Ответ на ваш вопрос буквально (как в названии) нет. Невозможно сделать это автоматически с помощью Entity Framework. В так называемых отключенных сценариях правильное сохранение изменений от клиента - это то, что разработчики должны заботиться о себе.
Как уже упоминалось, EF использовал объекты самоконтроля, но вскоре этот подход был устарел, хотя в официальной документации он никогда не был четко объяснен Зачем. Вероятно, потому что "STEs сделали (отслеживание изменений) проще, но ценой сделать почти все остальное действительно тяжело.." Он отлично подойдет API ObjectContext
с базовыми классами с базой данных с шаблонами t4, но, как мы все знаем, API-интерфейс DbContext
и первый код стали EF рекомендованной (и вскоре единственной поддерживаемой) архитектурой. Конечно, с кодовым первым EF не может обеспечить реализацию STE.
Или...?
Несколько разочаровывает тот факт, что EF никогда не заполнил этот пробел позже, например, предоставив API, аналогичный тому, что GraphDiff предлагает (или, возможно, к настоящему времени я должен сказать, предложил). Есть две разумные альтернативы, о которых я знаю.
Предложение структуры 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
Breeze предлагает полное решение, которое проходит весь путь от DAL в вашей службе до кода javascript на клиенте. Это невероятно удобно и невероятно страшно.
В javascript вы получаете синтаксис типа LINQ:
var query = breeze.EntityQuery
.from("Customers")
.where("CompanyName", "startsWith", "A")
.orderBy("CompanyName");
Это связывается с Breeze EntityManager
в коде С#:
var manager = new Breeze.Sharp.EntityManager(serviceName);
var results = await manager.ExecuteQuery(query);
Этот EntityManager
в основном представляет собой оболочку вокруг контекста EF. Если все движущиеся части настроены правильно, это фактически привносит контекст EF в ваш javascript, отслеживая изменения, сохраняя изменения и все. Я работаю с ним в одном проекте и действительно, это очень удобно.
Но если вы используете Breeze, это Breeze полностью. Это влияет на все. Изменение схемы базы данных требует изменений в javascript. Это страшно, но к чему вы привыкли. Но если вы хотите делать что-то по-своему, становится очень сложно (хотя и не невозможно) сгибать бриз в соответствии с вашими потребностями. Как жить со своей свекровью. Я думаю, во многих случаях, в конце концов, сочетание Бриз и других образцов становится неизбежным.
Но вам все равно нужно?
Вообще говоря, основной недостаток любого автоматизированного отслеживания отключенных объектов заключается в том, что слишком просто использовать оригинальные объекты объектов для передачи данных. Дело в том, что в большинстве случаев полные сущности содержат гораздо больше данных, чем требует клиент (или разрешено видеть). Использование выделенных тонких DTO может значительно повысить производительность. И, конечно же, они действуют как слой абстракции между DAL и UI/контроллерами.
Да, с DTO мы всегда должны "перекрашивать государство" на стороне сервера. Быть по сему. Это действительно рекомендуемый подход для отключенных сценариев.
Джон Папа, в своем курсе PluralSight по шаблону горячего полотенца для SPA, объясняя Бриз, признает эту проблему. Он предлагает решение с "частичными сущностями". Это решение, но довольно сложное и неуклюжее. И, конечно же, все объекты находятся в основе передачи данных.
Ответ 2
Как упоминалось в одном из предыдущих плакатов, это не поддерживается в EF и является одной из наиболее востребованных функций.
Однако, возможно, если вы захотите использовать библиотеку GraphDiff (или написать что-то подобное себе). Я настоятельно рекомендую вам использовать библиотеку open GraphDiff, чтобы обновлять отключенные графики в EntityFramework.
Просьба ознакомиться с этой статьей от Brent McKendrick за инструкциями о том, как это сделать.
Репозиторий github для этого проекта также можно найти здесь
Из приведенной выше статьи обновление графика связанных объектов так же просто, как в приведенном ниже примере:
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();
}
Обратите внимание, что этот проект больше не поддерживается, хотя я использовал его (как и многие другие) без каких-либо серьезных проблем.
Ответ 3
Посмотрите на следующие темы: Entity Framework 5 Обновление записи, Обновить отношения при сохранении изменений объектов EF4 POCO. Я чувствую, что он хорошо описывает все возможности и их плюсы и минусы. В основном каждая система отслеживания заставит вас хранить где-то исходную сущность для сравнения или запросить ее снова. Упомянутые объекты автотрассировки делают то же самое - вы должны хранить исходный объект в сеансе или состоянии просмотра, что ухудшит производительность страницы (здесь больше об этом: Объекты самоконтроля и объекты POCO). Что касается меня, я бы просто изменил состояние объекта на модифицированное и давал ef обновить базу данных (так же, как вы делаете это прямо сейчас в методе Put([FromBody]Word word)
), и это не только моя перспектива - Entity Framework: обновить связанные объекты.
Многие люди запрашивали такие возможности слияния (http://entityframework.codeplex.com/workitem/864), но пока EF не предоставляет никакой поддержки для этого. Однако некоторые разработчики отмечают, что есть некоторые альтернативы, такие как NHibernate, которые имеют встроенные возможности, такие как http://www.ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html.
Ответ 4
Да, вы можете, используя следующие библиотеки с низким уровнем воздействия, которые делают то, что вам нужно:
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- > ORM.Object
cfg.CreateMap<Word, ORM.Word>()
.ForMember(dest => dest.WordId, opt => opt.Ignore())