Метод Add(), добавляющий повторяющиеся строки для связанных моделей в Code-First Entity Framework

Ниже приведено действие, которое добавляет запрос на получение кредита в базу данных:

[HttpPost]
public ActionResult Add(Models.ViewModels.Loans.LoanEditorViewModel loanEditorViewModel)
{
    if (!ModelState.IsValid)
        return View(loanEditorViewModel);

    var loanViewModel = loanEditorViewModel.LoanViewModel;

    loanViewModel.LoanProduct = LoanProductService.GetLoanProductById(loanViewModel.LoanProductId); // <-- don't want to add to this table in database
    loanViewModel.Borrower = BorrowerService.GetBorrowerById(loanViewModel.BorrowerId); //<-- don't want to add to this table in database

    Models.Loans.Loan loan = AutoMapper.Mapper.Map<Models.Loans.Loan>(loanEditorViewModel.LoanViewModel);
    loanService.AddNewLoan(loan);
    return RedirectToAction("Index");
}

Ниже приведен метод AddNewLoan():

public int AddNewLoan(Models.Loans.Loan loan)
{
    loan.LoanStatus = Models.Loans.LoanStatus.PENDING;
    _LoanService.Insert(loan);

    return 0;
}

А вот код для Insert()

public virtual void Insert(TEntity entity)
{
    if (entity == null)
        throw new ArgumentNullException(nameof(entity));

    try
    {
        entity.DateCreated = entity.DateUpdated = DateTime.Now;
        entity.CreatedBy = entity.UpdatedBy = GetCurrentUser();

        Entities.Add(entity);
        context.SaveChanges();
    }
    catch (DbUpdateException exception)
    {
        throw new Exception(GetFullErrorTextAndRollbackEntityChanges(exception), exception);
    }
}

Он успешно добавляет одну строку в таблицу Loans, но также добавляет строки в таблицу LoanProduct и Borrower, как я показал в первых комментариях к коду.

Я проверил возможность многократного вызова этого действия и метода Insert, но они вызываются один раз.

ОБНОВИТЬ

Я столкнулся с аналогичной проблемой, но с противоположной функциональной проблемой здесь: Объект не обновляется с использованием подхода Code-First

Я думаю, что у этих двоих одна и та же причина отслеживания изменений. Но один добавляет другой не обновляет.

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

Vidmantas Blazevicius 30.05.2019 19:14

@VidmantasBlazevicius Я пытался использовать context.Entry(entity).State = EntityState.Detached и Entites.NoTracking() при получении LoanProduct и Borrower. Но это не работает.

Aishwarya Shiva 30.05.2019 20:19

Не добавляйте эти две строки: loanViewModel.LoanProduct = LoanProductService.GetLoanProductById(loanViewModel.LoanProd‌​uctId); // <-- don't want to add to this table in database loanViewModel.Borrower = BorrowerService.GetBorrowerById(loanViewModel.BorrowerId); //<-- don't want to add to this table in database

Saurabh Srivastava 03.06.2019 08:50

Automapper Создает новый объект «Заемщик» и «Продукт». Сопоставляли ли вы поля идентификатора в automapper?

halit 03.06.2019 09:38

@SaurabhSrivastava Если я этого не сделаю, то это бросает NullReferenceException

Aishwarya Shiva 03.06.2019 18:02

@halit Я даже пытался назначить эти два объекта после сопоставления AutoMapper, но безуспешно. Это все еще спасает их.

Aishwarya Shiva 03.06.2019 18:03

У вас нет сущности, которая сама по себе представляет таблицу Loan?

Ross Bush 03.06.2019 18:29

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

Paul Hatcher 03.06.2019 18:43

Это просто дубликат Дубликат DataType создается при каждом создании продукта. и многих других.

Gert Arnold 03.06.2019 21:32
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
9
350
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Следующий код кажется немного странным:

var loanViewModel = loanEditorViewModel.LoanViewModel;

loanViewModel.LoanProduct = LoanProductService.GetLoanProductById(loanViewModel.LoanProductId); // <-- don't want to add to this table in database
loanViewModel.Borrower = BorrowerService.GetBorrowerById(loanViewModel.BorrowerId); //<-- don't want to add to this table in database

Models.Loans.Loan loan = AutoMapper.Mapper.Map<Models.Loans.Loan>(loanEditorViewModel.LoanViewModel);

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

Вместо этого что-то вроде этого должно работать так, как ожидалось:

// Assuming these will throw if not found? Otherwise assert that these were returned.
var loanProduct = LoanProductService.GetLoanProductById(loanViewModel.LoanProductId);
var borrower = BorrowerService.GetBorrowerById(loanViewModel.BorrowerId);

Models.Loans.Loan loan = AutoMapper.Mapper.Map<Models.Loans.Loan>(loanEditorViewModel.LoanViewModel);
loan.LoanProduct = loanProduct;
loan.Borrower = borrower;

Редактировать:

Следующее, что нужно проверить, это то, что ваши службы используют ту же самую ссылку DbContext. Используете ли вы внедрение зависимостей с контейнером IoC, таким как Autofac или Unity? Если это так, убедитесь, что DbContext зарегистрирован как экземпляр на запрос или аналогичная область времени существования. Если Службы эффективно обновляют новый DbContext, то LoanService DbContext не будет знать об экземплярах Продукта и Заемщика, которые были получены DbContext другой службы.

Если вы не используете библиотеку DI, вам следует подумать о ее добавлении. В противном случае вам потребуется обновить свои службы, чтобы они принимали один DbContext при каждом вызове, или использовать шаблон единицы работы, такой как DbContextScope Mehdime, чтобы облегчить службам разрешение своего DbContext из единицы работы.

Например, чтобы обеспечить тот же DbContext:

using (var context = new MyDbContext())
{
    var loanProduct = LoanProductService.GetLoanProductById(context, loanViewModel.LoanProductId);
    var borrower = BorrowerService.GetBorrowerById(context, loanViewModel.BorrowerId);

    Models.Loans.Loan loan = AutoMapper.Mapper.Map<Models.Loans.Loan>(loanEditorViewModel.LoanViewModel);
    loan.LoanProduct = loanProduct;
    loan.Borrower = borrower;

    LoanService.AddNewLoan(context, loan);
}    

Если вы уверены, что все службы предоставляются одним и тем же экземпляром DbContext, то в вашем методе Entities.Add() может происходить что-то странное. Честно говоря, в вашем решении слишком много абстракции вокруг чего-то такого простого, как операция создания и ассоциации CRUD. Это похоже на случай преждевременной оптимизации кода для DRY, не начав с самого простого решения. Код может проще просто ограничить DbContext, выбрать применимые объекты, создать новый экземпляр, связать, добавить в DbSet и сохранить изменения. Нет никакой пользы в абстрагировании вызовов для элементарных операций, таких как выборка ссылки по идентификатору.

public ActionResult Add(Models.ViewModels.Loans.LoanEditorViewModel loanEditorViewModel)
{
    if (!ModelState.IsValid)
        return View(loanEditorViewModel);

    var loanViewModel = loanEditorViewModel.LoanViewModel;
    using (var context = new AppContext())
    {
       var loanProduct = context.LoanProducts.Single(x => x.LoanProductId == 
loanViewModel.LoanProductId);
       var borrower = context.Borrowers.Single(x => x.BorrowerId == loanViewModel.BorrowerId);
       var loan = AutoMapper.Mapper.Map<Loan>(loanEditorViewModel.LoanViewModel);
       loan.LoanProduct = loanProduct;
       loan.Borrower = borrower;
       context.SaveChanges();
    }
    return RedirectToAction("Index");
}

Посыпьте некоторой обработкой исключений, и все готово. Никаких многоуровневых сервисных абстракций. Оттуда вы можете стремиться сделать действие пригодным для тестирования, используя контейнер IoC, такой как Autofac, для управления контекстом и/или вводя шаблон репозитория/сервисного уровня /w UoW. Вышеизложенное будет служить минимально жизнеспособным решением для действия. Любая абстракция и т. д. должна применяться впоследствии. Сделайте набросок карандашом, прежде чем наносить масло. :)

Используя DbContextScope Mehdime, это будет выглядеть так:

public ActionResult Add(Models.ViewModels.Loans.LoanEditorViewModel loanEditorViewModel)
{
    if (!ModelState.IsValid)
        return View(loanEditorViewModel);

    var loanViewModel = loanEditorViewModel.LoanViewModel;
    using (var contextScope = ContextScopeFactory.Create())
    {
       var loanProduct = LoanRepository.GetLoanProductById( loanViewModel.LoanProductId).Single();
       var borrower = LoanRepository.GetBorrowerById(loanViewModel.BorrowerId);
       var loan = LoanRepository.CreateLoan(loanViewModel, loanProduct, borrower).Single();
       contextScope.SaveChanges();
    }
    return RedirectToAction("Index");
}

В моем случае я использую шаблон репозитория, который использует DbContextScopeLocator для разрешения ContextScope для получения DbContext. Репозиторий управляет получением данных и гарантирует, что при создании объектов будут предоставлены все необходимые данные, необходимые для создания полного и действительного объекта. Я выбираю репозиторий для каждого контроллера, а не что-то вроде общего шаблона или репозитория/сервиса для каждого объекта, потому что IMO это лучше управляет принципом единой ответственности, учитывая, что код имеет только одну причину для изменения (он обслуживает контроллер, а не разделяется между многими контроллеры с потенциально разными проблемами). Модульные тесты могут имитировать репозиторий для обслуживания ожидаемого состояния данных. Методы получения репо возвращают IQueryable, чтобы логика потребителя могла определить, как она хочет использовать данные.

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

Steve Py 06.06.2019 00:10

Проверьте Models.Loans.Loan? Это объединенная модель таблицы Loans, LoanProduct и Borrower.

Вы должны добавить

Loans  lentity = new Loans()
lentity.property=value;
Entities.Add(lentity );

var lentity = new Loans  { FirstName = "William", LastName = "Shakespeare" };
context.Add<Loans  >(lentity );
context.SaveChanges();
Ответ принят как подходящий

Наконец, с помощью ссылки, которой поделился @GertArnold Дубликат DataType создается при каждом создании продукта..

Поскольку все мои модели наследуют класс BaseModel, я изменил свой метод Insert следующим образом:

public virtual void Insert(TEntity entity, params BaseModel[] unchangedModels)
{
    if (entity == null)
        throw new ArgumentNullException(nameof(entity));

    try
    {
        entity.DateCreated = entity.DateUpdated = DateTime.Now;
        entity.CreatedBy = entity.UpdatedBy = GetCurrentUser();

        Entities.Add(entity);

        if (unchangedModels != null)
        {
            foreach (var model in unchangedModels)
            {
                _context.Entry(model).State = EntityState.Unchanged;
            }
        }

        _context.SaveChanges();
    }
    catch (DbUpdateException exception)
    {
        throw new Exception(GetFullErrorTextAndRollbackEntityChanges(exception), exception);
    }
}

И назвал это так:

_LoanService.Insert(loan, loan.LoanProduct, loan.Borrower);

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

Gert Arnold 06.06.2019 12:01

Безусловно, самый простой способ решить эту проблему — добавить два примитивных свойства внешнего ключа в класс Loan, то есть LoanProductId и BorrowerId. Например, вот так (я, очевидно, должен угадать типы LoanProduct и Borrower):

public int LoanProductId { get; set; }
[ForeignKey("LoanProductId")]
public Product LoanProduct { get; set; }

public int BorrowerId { get; set; }
[ForeignKey("BorrowerId")]
public User Borrower { get; set; }

Без примитивных свойств FK у вас есть так называемый независимые ассоциации, который можно установить только путем назначения объектов, состояние которых должно тщательно управляться. Добавление свойств FK превращает его в ассоциации внешних ключей, который легче установить. AutoMapper просто установит эти свойства, когда имена совпадут, и все готово.

Другие вопросы по теме