Update w relacji wiele do wielu

0

Cześć,
Zderzyłem się dzisiaj nieprzyjemnie z updatem danych w realcji wiele do wielu korzystając z EF Core (swoją drogą, napotkałem w tym frameworków sporo niedogodności, chociaż to może tylko błędne wrażenie początkującego). Liczyłem po cichu na to, że jeżeli wpadnie mi update obiektu wiele do wielu to taki kod wystarczy:

public Meal UpdateMeal(Meal updatedMeal)
        {
            dbContext.Meals.Update(updatedMeal);
            dbContext.SaveChanges();
            return updatedMeal;
        }

Niestety powyższy przy save'ie zwraca exception:

SqlException: Violation of PRIMARY KEY constraint 'PK_MealProducts'. Cannot insert duplicate key in object 'dbo.MealProducts'. The duplicate key value is (12, 19). The statement has been terminated.

Te zduplikowane wartości to pierwszy "pod-obiekt" z wielu, więc zakładam że zamiast updetować, EF Core zamierzał dodać te rekordy na nowo, przez co zwróciło exception'a. Pytanie jak z tym sobie poradzić?

0

Znalazłem na SO taki wpis Entity Framework Core: Violation of PRIMARY KEY constraint Cannot insert duplicate key in object

Pokaż klasę Meal. Jeśli ta klasa zawiera odwołanie do wielu MealProducts to może dopisanie atrybutu [ForeignKey("meal_id")] pomoże

0

@AdamWox:
@mariano901229:

Klasa Meal:

    public class Meal
    {
        public int Id { get; set; }

        public int? TypeOfMealId { get; set; }
        public TypeOfMeal TypeOfMeal { get; set; }
        public string MealName { get; set; }
        public string Description { get; set; }
        public int Kcal { get; set; } = 0;
        [ForeignKey("MealId")]
        public virtual List<MealProduct> MealProducts { get; set; }
        public virtual List<DayDietMeals> DayDietMeals { get; set; }
    }

Metoda kontrolera, która wywołuje metodę UpdateMeal:

        public IActionResult OnPost(Meal meal)
        {
            CalculateKcal(meal);
            mealRepository.UpdateMeal(meal);
            return RedirectToPage("Index");
        }
0

Czy w MealProduct ma faktycznie MealId? Czy inaczej ten klucz się tam nazywa?

1

A nie dodajesz tam do kolekcji obiektów, których nie śledzi changetracker, bo wrzucasz to z jakiegoś dtosa i po dodaniu do contextu mają state - added, zamiast modified?

0

Prawdopodobnie EF MealProducts traktuje jako nowy i go dodaje. Mimo, iż w parametrze podaje dane do aktualizacji to dla EF MealProducts jest z innego "kontekstu" i robi insert. Jesteś w stanie zrobić jakiś debug i sprawdzić jaki status ma MealProducts. Dla testu również możesz spróbować wyciągnąć MealProducts z bazy i przypisać do Meal, który wpada z parametru.

@urke
To samo pomyślałem. Jego MealProducts jest z innego kontekstu i ma status added zamiast modified.

0
AdamWox napisał(a):

Prawdopodobnie EF MealProducts traktuje jako nowy i go dodaje. Mimo, iż w parametrze podaje dane do aktualizacji to dla EF MealProducts jest z innego "kontekstu" i robi insert. Jesteś w stanie zrobić jakiś debug i sprawdzić jaki status ma MealProducts. Dla testu również możesz spróbować wyciągnąć MealProducts z bazy i przypisać do Meal, który wpada z parametru.

@urke
To samo pomyślałem. Jego MealProducts jest z innego kontekstu i ma status added zamiast modified.

Chodzi o coś takiego?
screenshot-20210112211826.png

@urke Poniżej cała klasa edit, najpierw pobieram po id meal z bazy i przypisuje go do właściwości Meal w tej klasie, później w Calculate jest obliczenie jednej właściowości i mapowanie, a następnie idzie update.

public class EditModel : PageModel
    {
        private readonly IMealRepository mealRepository;
        private readonly IProductRepository productRepository;
        private readonly ITypeOfMealRepository typeOfMealRepository;

        public Meal Meal { get; set; }
        public IEnumerable<Product> Products { get; set; }
        public IEnumerable<TypeOfMeal> TypeOfMeals { get; set; }
        public EditModel(IMealRepository mealRepository,
            IProductRepository productRepository,
            ITypeOfMealRepository typeOfMealRepository)
        {
            this.mealRepository = mealRepository;
            this.productRepository = productRepository;
            this.typeOfMealRepository = typeOfMealRepository;
        }

        public IActionResult OnGet(int Id)
        {
            TypeOfMeals = typeOfMealRepository.GetAllTypes();
            Products = productRepository.GetAllProducts();
            Meal = mealRepository.GetMeal(Id);
            return Page();
        }

        public IActionResult OnPost(Meal meal)
        {
            CalculateKcal(meal);
            mealRepository.UpdateMeal(meal);
            return RedirectToPage("Index");
        }

        private void CalculateKcal(Meal meal)
        {
            var mealProducts = new List<MealProduct>();
            foreach (var mealProduct in meal.MealProducts)
            {
                if (mealProduct.ProductId != null & mealProduct.Quantity != null)
                {
                    var product = productRepository.GetProduct((int)mealProduct.ProductId);
                    meal.Kcal = (int)(meal.Kcal + (mealProduct.Quantity * product.Kcal / product.QuantityUnit));
                    mealProducts.Add(mealProduct);
                }
            }
            Meal = new Meal()
            {
                Id = meal.Id,
                Description = meal.Description,
                MealName = meal.MealName,
                TypeOfMeal = meal.TypeOfMeal,
                TypeOfMealId = meal.TypeOfMealId,
                MealProducts = mealProducts,
                Kcal = meal.Kcal
            };
        }
    }
0

To jest tak zwane podejście encja na twarz i pchasz.

Używasz modelu domenowo/ormowego do wypychania na widok/wysyłania z widoku i pchasz go od razu do orma, a on nie ma pojęcia o tym, że ten obiekt był pobrany z bazy i już tam istnieje.

Jak już chcesz takie podejście, to w metodzie update, musisz wyłapywać obiekty, które już mają klucz główny, dodawać do obecnego contextu i ustawiać im EntityState = EntityState.Modified.

(Jeśli się mylę, to niech ktoś mnie poprawi, bo się nie musiałem męczyć z tym hatfu ormem już chwilkę, bo może coś nazmieniali).

0

Wydaje mi się, że jak wyciągniesz przed update'em MealProducts z dbContext to powinno zadziałać.

public Meal UpdateMeal(Meal updatedMeal)
{
      updatedMeal.MealProducts = dbContext.MealProducts.Where(x => x.MealId == updatedMeal.Id);
      dbContext.Meals.Update(updatedMeal);
      dbContext.SaveChanges();
      return updatedMeal;
}

#EDIT
Wiadomo, że to nie ma sensu, ponieważ mogły się zmienić składniki (produkty). Więc pewnie w pętli trzeba będzie to puścić i "poprawić", ale prawdopodobnie wtedy EF dowie się, że to są dany z bazy, a nie nowe obiekty.

0

Ok bo już chyba posunąłem się o krok do przodu (a przynajmniej takie mam wrażenie). Otrzymuję obecnie taki błąd:
Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.”

Z kolei w debugerze mam informacje, że śledzony jest nie tylko sam Meal , ale także MealProducts (wcześniej był tylko Meal).
screenshot-20210115171529.png

Dodam tylko, że dodałem nowy produkt podczas edycji o Id=6, ale ma status Modified, a chyba powinien mu nadać Added, bo o takim kluczu nie ma żadnego rekordu w MealProducts

EDIT

Dla tych, którzy szukaliby rozwiązania. Poradziłem sobie z tym trochę na piechotę:

public Meal UpdateMeal(Meal updatedMeal)
        {
            var existingMeal = dbContext.Meals.Where(m => m.Id == updatedMeal.Id).Include(c => c.MealProducts).SingleOrDefault();

            if (existingMeal != null)
            {
                // Update parent
                dbContext.Entry(existingMeal).CurrentValues.SetValues(updatedMeal);

                // Delete children
                foreach (var existingMealProduct in existingMeal.MealProducts)
                {
                    if (!updatedMeal.MealProducts.Any(c => c.MealId == existingMealProduct.MealId && c.ProductId == existingMealProduct.ProductId))
                        dbContext.MealProducts.Remove(existingMealProduct);
                }

                // Update and Insert children
                foreach (var newOrUpdateMealProduct in updatedMeal.MealProducts)
                {
                    var existingMealProduct = existingMeal.MealProducts
                        .Where(c => c.MealId == newOrUpdateMealProduct.MealId && c.MealId != default(int) && c.ProductId == newOrUpdateMealProduct.ProductId && c.ProductId != default(int))
                        .SingleOrDefault();

                    if (existingMealProduct != null)
                        // Update child
                        dbContext.Entry(existingMealProduct).CurrentValues.SetValues(newOrUpdateMealProduct);
                    else
                    {
                        // Insert child
                        var newMealProduct = new MealProduct
                        {
                            MealId = newOrUpdateMealProduct.MealId,
                            ProductId = newOrUpdateMealProduct.ProductId,
                            Quantity = newOrUpdateMealProduct.Quantity
                        };
                        existingMeal.MealProducts.Add(newMealProduct);
                    }
                }

                dbContext.SaveChanges();
                return updatedMeal;
            }
            return updatedMeal;
        }

1 użytkowników online, w tym zalogowanych: 0, gości: 1