EF 4.1 Заменить строку?

У меня проблема, когда внутри класса я хочу обновить или добавить строку в зависимости от записи, существующей внутри БД. В этом случае вместо методов «Создать/обновить» у меня есть метод «Сохранить (сущность)».

Идея заключается в том, что объект проверяется по базе данных, если он существует, то это обновление, если нет, то, очевидно, это создание.

При использовании EF 4.1 проблема заключается в том, что как только я читаю строку из базы данных через тот же контекст, это, в свою очередь, создает кеш памяти этой строки. Когда я затем попытаюсь заменить строку, скажем, с помощью процедуры присоединения/добавления, она, очевидно, вызовет исключение для уже существующей строки и т. д. (поскольку она пытается принудительно поместить как currentRow, так и newRow в одну и ту же таблицу и терпит неудачу при согласовании).

Мой обходной путь для этого заключается в том, чтобы использовать .Remove() в контексте, который помечает строку для удаления из памяти, а затем повторно добавляет ее в ту же транзакцию USING.

 var ctx = new SecurityContext(this.ConnectionString);
        using(ctx)
        {
            var dbEntry = (ctx.Accounts.Where(a => a.AccountId == entity.AccountId || a.Username == entity.Username)).FirstOrDefault();
            ctx.Accounts.Remove(dbEntry);

            if (dbEntry != null)
            {
                ctx.Entry(entity).State = EntityState.Added;
            } else
            {
                ctx.Accounts.Add(entity);
                ctx.Entry(entity).State = EntityState.Added;
            }


            ctx.SaveChanges();
        }

Мой вопрос: это типичный маршрут? или есть более умные/чистые способы?


person Scott Barnes    schedule 11.09.2011    source источник
comment
Не могли бы вы использовать Accounts.Any(...) для проверки существования записи, чтобы не получить ее новую копию из базы данных?   -  person Matt Hamilton    schedule 12.09.2011
comment
Хороший вопрос. Я попытался использовать Any, но получил такое же исключение, т.е. var dbEntry = (ctx.Accounts.Any(a => a.AccountId == entity.AccountId || a.Username.Contains(entity.Username))); ctx.Accounts.Add(объект); ctx.Entry(entity).State = dbEntry ? EntityState.Modified : EntityState.Added; ctx.СохранитьИзменения();   -  person Scott Barnes    schedule 12.09.2011
comment
Что, если вы используете .Attach(entity), а не .Add(entity)? Возможно, использование .Add() заставляет предположить, что сущность должна быть вставлена.   -  person Matt Hamilton    schedule 12.09.2011
comment
Я добавил ответ на случай, если мы на правильном пути. Это лучше, чем длинный поток комментариев.   -  person Matt Hamilton    schedule 12.09.2011


Ответы (4)


Я считаю, что этот код должен работать, используя Attach, а не Add:

var  ctx = new SecurityContext(this.ConnectionString); 
using(ctx) 
{ 
    ctx.Accounts.Attach(entity);
    ctx.Entry(entity).State = ctx.Accounts.Any(
        a => a.AccountId == entity.AccountId || 
        a.Username == entity.Username) ? 
        EntityState.Modified : EntityState.Added;
    ctx.SaveChanges(); 
} 

Простите за странную обертку — хотел, чтобы она поместилась на странице без прокрутки.

person Matt Hamilton    schedule 11.09.2011

Хорошо, я думаю, что это был случай, когда я был тупицей. Я понял, что конфликт мог возникнуть, когда две записи были созданы точно так же, за исключением того, что AccountId были разными (учитывая, что они были сгенерированы через класс вызывающей стороны). Я изменил DAO, чтобы он был ниже, и теперь он работает.

Разделите код, если хотите :)

    public class AccountDAO : BaseDAO<Account>
{
    public AccountDAO(string connectionString) : base(connectionString)
    {
    }
    public override Account Save(Account entity)
    {
        var ctx = new SecurityContext(this.ConnectionString);
        using(ctx)
        {
            //var dbEntry = (ctx.Accounts.Where(a => a.AccountId == entity.AccountId || a.Username == entity.Username)).FirstOrDefault();
            var dbEntry = (ctx.Accounts.Any(a => a.AccountId == entity.AccountId || a.Username.Contains(entity.Username)));
            //ctx.Accounts.Remove(entity);
            if(!dbEntry)
            {
                ctx.Accounts.Add(entity);
                ctx.Entry(entity).State = EntityState.Added;
            } else
            {
                var currEntity = Read(entity);
                entity.AccountId = currEntity.AccountId;
                ctx.Accounts.Add(entity);
                ctx.Entry(entity).State = EntityState.Modified;
            }

            ctx.SaveChanges();
        }
        return entity;
    }
    public override Account Read(Account entity)
    {
        using (var ctx = new SecurityContext(this.ConnectionString))
        {
            var newEntity = (ctx.Accounts.Where(a => a.AccountId == entity.AccountId || a.Username.Contains(entity.Username))).FirstOrDefault();
            return newEntity;
        }            
    }
    public override void Delete(Account entity)
    {
        using (var ctx = new SecurityContext(this.ConnectionString))
        {
            var ent = Read(entity);
            ctx.Entry(ent).State = EntityState.Deleted;
            ctx.Accounts.Remove(ent);
            ctx.SaveChanges();
        }     
    }
}
person Scott Barnes    schedule 11.09.2011
comment
Будет ли работать мой меньший фрагмент кода, если вы измените предложение .Any(...) на новое? Это все еще кажется намного большим количеством кода, чем необходимо. - person Matt Hamilton; 12.09.2011

см. образец

 private void Save(Action<Controls.SaveResult> saveResult)
    {
        if (SavableEntity.EntityState != EntityState.Unmodified)
        {
            if (SavableEntity.EntityState == EntityState.Detached || SavableEntity.EntityState == EntityState.New)
                this.SetIsBusy("Creating new...");
            else
                this.SetIsBusy("Saving changes...");
            DomainContext.SavePartial(SavableEntity, p =>
            {
                this.ReleaseIsBusy();
                if (p.HasError)
                {
                    var res = new Controls.SaveResult() { HasErrors = true };
                    if (saveResult != null)
                        saveResult(res);
                    if (this.EntitySaved != null)
                    {
                        EntitySaved(this, new SavedEventArgs() { result = res });
                    }
                    p.MarkErrorAsHandled();
                }
                else
                {
                    Messenger.Default.Send<T>(this.SavableEntity);
                    var res = new Controls.SaveResult() { HasErrors = false };
                    if (saveResult != null)
                        saveResult(res);
                    if (this.EntitySaved != null)
                        EntitySaved(this, new SavedEventArgs() { result = res });
                    if (this.CloseAfterSave)
                        this.RaiseRequestToClose();
                    this.RaisePropertyChanged("Title");
                }
                RaisePropertyChanged("SavableEntity");

            }, false);
        }

    }
person gregg Dourgarian    schedule 11.09.2011

Возможно, вы так сформулировали вопрос, но у вас, похоже, странный подход.

  1. Если вы извлекаете и обновляете объект на одном уровне приложения, вам не следует воссоздавать свой контекст при сохранении, просто используйте ссылку на контекст, который извлекает объект, и он будет отслеживать изменения в вашем объекте. и вы просто вызываете SaveChanges() в том же контексте. В противном случае вы боретесь с его фундаментальной конструкцией.

  2. Вы должны иметь привычку оборачивать SaveChanges() в транзакцию. Если SaveChanges() инициирует изменения/вставки в более чем 1 строку в базе данных, вы рискуете сохранить частичные изменения. Вы всегда должны сохранять все или ничего.

    using (TransactionScope ts = new TransactionScope()) {
    ctx.SaveChanges();
    ts.Complete(); }

  3. Если вы разрабатываете трехуровневое приложение, используя, возможно, wcf для среднего уровня и, следовательно, сериализуя объект от клиента, вы можете просто добавить новое свойство «IsNew», через которое проходит клиент. Если не новый, вы должны использовать Attach(). например, если IsNew, то ctx.Accounts.Add(entity), иначе ctx.Accounts.Attach(entity)

  4. Предполагая вышеизложенное, если у вас есть объект IsNew, но вы хотите убедиться, что он не существует, в качестве окончательной проверки на вашем среднем уровне (я предполагаю, что ваш клиент уже попытался разрешить пользователю редактировать существующий объект, если он существует). Сначала вы должны установить ограничение уникальности базы данных, так как это ваша последняя защита от дубликатов. Во-вторых, вы можете использовать подход, который у вас уже есть, когда вы проверяете, существует ли объект в базе данных, а затем либо объединяете объекты вручную, если это необходимая вам функциональность, либо генерируете исключение/исключение параллелизма, которое заставляет клиент перезагрузить реальная сущность, и они могут изменить ее.

Пункт 4 довольно сложен, и есть ряд подходов, которые слишком сложны, чтобы я пытался их описать. Но будьте осторожны, если вы используете свой подход, когда вы проверяете его существование, а затем решаете добавить/присоединить, убедитесь, что вы завернули это в транзакцию, так как в противном случае есть вероятность, что новый объект будет добавлен другим пользователем/процессом между ними, когда вы проверяете (с помощью Where()) и SaveChanges().

person user381624    schedule 12.09.2011