Entity Framework: как удалить дочерний элемент, используя общий шаблон репозитория?

Эта проблема несколько раз обсуждалась в stackoverflow, однако я не смог найти ответа о том, как она была решена с использованием общего шаблона репозитория. Все данные ответы используют DBContext напрямую. В общем шаблоне репозитория у меня не будет прямого доступа к DBContext, также я использую Unity для IOC.

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

Операция завершилась неудачно: связь не может быть изменена, так как одно или несколько свойств внешнего ключа не могут принимать значения NULL. Когда в отношения вносятся изменения, для связанного свойства внешнего ключа устанавливается нулевое значение. Если внешний ключ не поддерживает значения NULL, необходимо определить новую связь, свойству внешнего ключа необходимо присвоить другое значение, отличное от NULL, или необходимо удалить несвязанный объект.

Теперь я не знаю, почему EF пытается установить для FK значение null вместо того, чтобы просто удалить запись. Какова цель установки FK на ноль, но сохранения потерянной записи в БД.

В любом случае, как мне решить эту проблему, используя шаблон репозитория? Нужно ли выставлять какой-либо новый метод из репозитория?

Объекты

    public class parent
    {
        public int ParentID {get;set;}  //Primary Key

        public string ParentName {get;set}

        public ICollection<Child> Children {get;set}
    }

    public class Child
    {
        public int ChildID {get;set;}  //Primary Key

        public string ChildName {get;set;}

        public int ParentID {get;set;}  //Foreign Key
    }

Сервис

    public class MyService
    {
        private IGenericRepository _repository;

        public MyService(IGenericRepository repository)
        {
          _repository = repository;
        }

        public void UpdateParent(int parentID,string parentName, int[] sourceChildIDs)
        {
            var p = _repository.GetQuery<Parent>()
                .Include(x => x.Children)
                .Where(x => x.ParentID == parentID)
                .SingleOrDefault();

            p.ParentName = parentName;

            var childrenToDetete = new List<Child>();
            foreach (var child in p.Children)
            {
                if (!sourceChildIDs.Contains(child.ChildID))
                {
                    childrenToDetete.Add(child);
                }
            }

            foreach (var child in childrenToDetete)
            {
                p.Children.Remove(child);
            }            

            _repository.SaveChanges(); // i get error here
        }
    }

Репозиторий

    public class GenericRepository : IGenericRepository
    {

        private DbContext _dbContext;        


        public GenericRepository(DbContext dbContext)
        {
            if (dbContext == null)
            {
                throw new ArgumentNullException("dbContext");
            }

            _dbContext = dbContext;
        }


        public TEntity Create<TEntity>() where TEntity : class
        {
            return _dbContext.Set<TEntity>().Create<TEntity>();
        }

        public TEntity Add<TEntity>(TEntity entity) where TEntity : class
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }

            return _dbContext.Set<TEntity>().Add(entity);
        }

        public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
        {
            return _dbContext.Set<TEntity>();
        }

        public IQueryable<TEntity> GetQuery<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
        {
            return GetQuery<TEntity>().Where(predicate);
        }    

        public void Delete<TEntity>(TEntity entity) where TEntity : class
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }

            _dbContext.Set<TEntity>().Remove(entity);
        }

        public void Delete<TEntity>(Expression<Func<TEntity, bool>> criteria) where TEntity : class
        {
            IEnumerable<TEntity> records = GetQuery<TEntity>(criteria);

            foreach (TEntity record in records)
            {
                Delete<TEntity>(record);
            }
        }

        public void Update<TEntity>(TEntity entity) where TEntity : class
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }

            _dbContext.Entry(entity).State = EntityState.Modified;
        }

        public int SaveChanges()
        {
            return _dbContext.SaveChanges();
        }       
    }

person LP13    schedule 26.05.2016    source источник


Ответы (1)


Вы удаляете дочерние элементы из родительского объекта, но не из базы данных. В качестве примечания, вы можете сделать это более кратким способом:

foreach (var child in p.Children
                       .Where(child => !sourceChildIDs.Contains(child.ChildID))
                       .ToList())
{
    p.Children.Remove(child);
}

Но это только разрушает связь между родителями и детьми. EF проявляет осторожность и предполагает, что вы хотите удалить только ссылку на внешний ключ дочернего элемента, а не удалять дочерние элементы полностью.

Поэтому вам нужно удалить дочерние элементы из базы данных, заменив предыдущие операторы на

var delIds = p.Children.Where(child => !sourceChildIDs.Contains(child.ChildID))
                       .Select(c => c.ChildID).ToList();
_repository.Delete<Child>(c => delIds.Contains(c.ChildID));

Кстати, это довольно необычная реализация универсального репозитория. Обычно общие репозитории создаются для одного типа, то есть определение будет GenericRepository<T>. Экземпляры этих репозиториев обычно совместно используют один экземпляр контекста, в то время как они взаимодействуют в одной единице работы, которая также сохраняет изменения.

person Gert Arnold    schedule 26.05.2016
comment
Я думаю, мне все еще нужно рассчитать delid - person LP13; 27.05.2016
comment
Забыл одну строчку ;) - person Gert Arnold; 27.05.2016
comment
Спасибо .. Если я реализую GenericRepository<T> для каждого типа, как вы собираетесь вводить GenericRepository<T> в MyService. Эта служба имеет несколько методов и должна иметь возможность запрашивать любой объект. - person LP13; 27.05.2016
comment
На самом деле, я не вижу никакой пользы в универсальных репозиториях поверх репозиториев EF (DbSet), поэтому никогда ими не пользуюсь. Но они, безусловно, могут быть внедрены любым приличным контейнером IoC. Вы должны быть в состоянии найти примеры для инфраструктуры DI по вашему выбору. Если нет, задайте новый вопрос, конкретно касающийся этой части. - person Gert Arnold; 27.05.2016
comment
я думаю, что наличие собственного репозитория поможет вам в модульном тестировании, так что вы сможете пройти фиктивный репозиторий. А также, если вы меняете источник данных, вам нужно только подключить новый репозиторий без изменения бизнес-логики на уровне сервиса. - person LP13; 27.05.2016
comment
Имитировать репозитории нетривиально. Я предполагаю, что когда вы говорите изменить источник данных, вы имеете в виду замену уровня доступа к данным, например, на NHibernate. Это тоже не так просто, как кажется из-за дырявых абстракций. - person Gert Arnold; 29.05.2016