Aktualizacja w bazie danych tylko zmienionych pól

0
somekind napisał(a):

Ten sam efekt uzyskasz wysyłając całość danych z frontu na backend.

Jak to wtedy ma działać? Np.

Kontekst bazy danych

public DbSet<UserSettings> UserSettings { get; set; }

Encje:

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

    public DateTime LastChange {get; set;}

    public string NotifyEmail {get; set;}

    public ICollection<NotificationSchedule> NotificationSchedules {get; set;}
}

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

    public TimeSpan From {get; set;}

    public TimeSpan To {get; set;}

    public int MaxEmailsAmount {get; set;}
}


Aktualizacja rekordu w serwisie

public void UpdateUserSettings(UserSettings settings)
{
    using (var context = new SettingsContext())
    {   
        var entity = context.UserSettings
            .FirstOrDefault(x => x.Id == settings.Id);

        if (entity == null)
        {
            throw new Exception("Entity not found");
        }

        context.Entry(entity).CurrentValues.SetValues(settings);    
        entity.NotificationSchedules = settings.NotificationSchedules;

        context.SaveChanges();
    }
}

Użycie w kliencie

var service = new SettingsService();
var settings = service.GetAllUserSettings.First();

settings.NotifyEmail = "[email protected]";
settings.NotificationSchedules.First().MaxEmailsAmount = 10;

service.UpdateUserSettings(settings);

Po tej operacji adres email ustawień się pomyślnie zmienił na [email protected], ale MaxEmailsAmount jest bez zmian. Ogólnie to średnio mi się to podoba. Jakoś tak średnio ufam EF, że wszystko ładnie zaktualizuje, albo usunie to co miał usunąć..

2

Hmm, coś mi tu nie gra.

W UpdateUserSettings w linii 14 powinieneś mieć nulla więc to i tak nie powinno zadziałać moim zdaniem.

Ja bym nawet tak nie edytował tych NotificationSchedules. Jeśli już się uprzeć na edycję całych modeli to przede wszystkim wystawiłbym dwie metody do edycji: UpdateUserSettings oraz UpdateNotificationSchedule. Nie wiem czy robisz to w API, ale zakładając, że tak to bym stworzył modele wejściowe z api:

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

        public string NotifyEmail { get; set; }
    }

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

        public TimeSpan From { get; set; }

        public TimeSpan To { get; set; }

        public int MaxEmailsAmount { get; set; }
    }

Zrobił AutoMapperem profile mapowania na encje do bazy i w każdej z tych dwóch metod serwisu przyjmowałbym odpowiedni model i na przykład serwisy wyglądałyby tak:

public void UpdateUserSettings(UpdateUserSettingsApiRequest settings)
{
    using (var context = new SettingsContext())
    {   
        var entity = context.UserSettings
            .FirstOrDefault(x => x.Id == settings.Id);

        if (entity is null)
        {
            throw new Exception("Entity not found");
        }

        Mapper.Map(settings, entity);  
        context.SaveChanges();
    }
}

public void NotificationSchedule(UpdateNotificationScheduleApiRequest notificationSchedule)
{
    using (var context = new SettingsContext())
    {   
        var entity = context.NotificationSchedules
            .FirstOrDefault(x => x.Id == notificationSchedule.Id);

        if (entity is null)
        {
            throw new Exception("Entity not found");
        }

        Mapper.Map(notificationSchedule, entity);  
        context.SaveChanges();
    }
}

Owszem, można to zrobić jedną metodą i np tak:

public void UpdateUserSettings(UpdateUserSettingsApiRquest settings)
{
    using (var context = new SettingsContext())
    {   
        var entity = context.UserSettings
            .Include(us => us.NotificationSchedules)
            .FirstOrDefault(x => x.Id == settings.Id);

        if (entity is null)
        {
            throw new Exception("Entity not found");
        }

        Mapper.Map(settings, entity);  

       foreach (var notificationSchedule in settings) 
       {
            var notificationScheduleEntity = entity.NotificationSchedules.FirstOrDefault(_ => _.Id == notificationSchedule.Id);
            if (notificationScheduleEntity != null) 
            {
                   Mapper.Map(notificationSchedule, notificationScheduleEntity);
            }
       }
        context.SaveChanges();
    }
}

Tak na szybko przykład pisany tutaj więc nie wiem czy się nawet skompiluje :)

Oczywiście model wejściowy w ostatnim przykładzie zawiera kolekcję NotificationSchedule.

2

@west:

var entity = context.UserSettings.FirstOrDefault(x => x.Id == settings.Id);

czy tu nie brakuje Includa? Include(x => x.NotificationSchedules) bo chyba bez tego tracking może nie działać tak jak oczekujesz

0

@lukaszek016
Średnio podoba mi się pomysł tworzenia nowych metod i tych obiektów ApiRequest. Mając użytkownika z 10 kluczami obcymi, musiałbym stworzyć 10 metod, 10 dodatkowych klas ApiRequest, oraz 10 konfiguracji Automappera. Dodatkowo, miałbym 10 strzałów do bazy danych. Trzebaby też zadbać o to, by to wszystko było 1 dużą transakcją.. Mam wrażenie, że szybko o duży bałagan w kodzie..

@WeiXiao
Pisałem to trochę z palca. Właściwie, to dostaję wyjątek:

System.Data.Entity.Core.EntityException
  HResult=0x80131501
  Message=Unable to set field/property CredentialsList on entity type UserSettings. See InnerException for details.
  Source=<Cannot evaluate the exception source>
  StackTrace:
<Cannot evaluate the exception stack trace>

Inner Exception 1:
InvalidOperationException: An item cannot be removed from a fixed size Array of type 'NotificationSchedule[]'.

Mam wrażenie, że działanie bezpośrednio na kontekście bazy danych to znacznie prostsze i mniej błędogenne rozwiązanie. Zmieniając ustawienia wprowadzam zmiany bezpośrednio do kontekstu, a kliknięcie w przycisk wywoła SaveChanges(). Kontekst wie co zostało zmienione, więc wyśle odpowiedni sql do systemu bazodanowego. Działanie bezpośrednio na kontekście rozwiązuje też potrzeby includowania. Niby proste, fajne i przyjemne, ale podobno tak się nie robi :/

Te "dane lokalne" zrobili chyba po to, by z tego korzystać: https://docs.microsoft.com/pl-pl/ef/ef6/querying/local-data

1

@west:

wait what

Mam wrażenie, że działanie bezpośrednio na kontekście bazy danych to znacznie prostsze i mniej błędogenne rozwiązanie. Zmieniając ustawienia wprowadzam zmiany bezpośrednio do kontekstu, a kliknięcie w przycisk wywoła SaveChanges(). Kontekst wie co zostało zmienione, więc wyśle odpowiedni sql do systemu bazodanowego. Działanie bezpośrednio na kontekście rozwiązuje też potrzeby includowania. Niby proste, fajne i przyjemne, ale podobno tak się nie robi

1: co masz na myśli działanie pisząc "bezpośrednio na kontekście"
2: dlaczego niby działanie bezpośrednio na kontekście rozwiązuje potrzebne używania includa gdy chcesz zrobić joina?
3: Niby proste, fajne i przyjemne, ale podobno tak się nie robi zależy, "zdania ekspertów są podzielone" :P

1

@west: no przestań, nie przesadzaj. Zacznijmy od tego, że Twój user nie ma póki co ani jednego klucza obcego, bo gdyby tak było to można by to jeszcze inaczej rozwiązać i do serwisu wrzucać płaski model danych do edycji, ale to też zależy od konkretnego przypadku. Obecnie masz tak, że to NotificationSchedule ma klucz obcy do Usera :)

Druga sprawa to AutoMapper, no niestety, coś za coś, jednak tworzenie map nie jest chyba zbyt uciążliwe, bo jeśli model źródłowy ma nawet mniej pól, niż docelowy, ale typy i nazwy się zgadzają to nie musisz ręcznie definiować mapowania dla każdego pola. Poza tym pomyśl o tym, że Twój User może mieć właśnie 10 czy 30 kluczy obcych, 40 innych pól, których nie wykorzystujesz na widoku i co, taki user-kombajn będzie latał pomiędzy warstwami z całym zestawem danych? Wg mnie powinno być tak, że mój model wyjściowy zawiera tylko to co jest mi potrzebne akurat na widoku. Tak naprawdę to wszystko zależy od konkretnego przypadku i na przykład: jak masz duży model usera i na widoku wyświetlasz kilka zakładek z jego danymi to dobrym rozwiązaniem byłoby każdą zakładkę ładować oddzielnym zapytaniem czy strzałem do API na żądanie i pobierać tylko te dane, których akurat potrzebujesz.
Nie namawiam do AutoMappera, jedni mówią, że jest zły, inni, że nie, mi akurat odpowiada, a ProjectTo używam bardzo chętnie.

Jeśli zależy Ci tak bardzo na wydajności bazy danych oraz ograniczeniu ilości zapytań to dobre wyjście - przestań korzystać z EntityFrameworka. Odpal Profilera i zobaczysz co tam się czasami dzieje ;)

Po drugie to nie jest do końca tak, że jak zapakujesz całą logikę do jednej metody i na koniec użyjesz SaveChanges to spowoduje, że wygenerowane zostanie jedno zapytanie, oj nie :P Zrób test: zrób, żeby Twoja metoda działała, a potem użyj mojej z ostatniego przykładu i porównaj wygenerowany kod SQL i sam zobaczysz.

0

@lukaszek016: Proponujesz więc coś takiego jak tutaj:
https://stackoverflow.com/que[...]per-update-nested-collections

W jaki sposób radzisz sobie z tym problemem, który został opisany, tzn:

The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.

0

@pikob: Chciałem wykorzystać ORM

Udało mi się skleić coś takiego:
https://github.com/westpop/ServiceEdit

Najwięcej inspirowałem się tym, co napisał @lukaszek016. Dodatkowo musiałem wykorzystać Automapper.Collections, aby działało poprawnie mapowanie kolekcji. Zdaje się, że wszystko działa. @WeiXiao @somekind @lukaszek016 zerkniecie na ten przykładowy projekt w github i powiecie czy tak jest ok?
Głównie chodzi mi o te pliki:
Serwis
Inicjalizacja Automappera
Kontekst bazy danych

Troche boję się, że coś może nie zadziałać w przyszłości, np. ktoś zapomnie jakiegoś include przy pobieraniu danych z kontekstu, albo zmieni nazwę właściwości, przez co Automapper czegoś zapomni zmapować i w rezultacie jakieś informacje z bazy danych zostaną utracone.. No cóż, niczego lepszego nie udało mi się zrobić.

0

@west: Myślę, że co do samego pytania jak to rozwiązać musisz wiedzieć kilka rzeczy:

  1. baza jeżeli zostanie wywołana do zapisu, to nie patrzy co nadpisuje, i tak nadpisze, nawet jeżeli dane są te same
  2. to rozwiązanie będzie i tak szybkie, musiałbyś mieć miliony userów żeby to było mało wydajne, poza tym nie ma powodów żeby się czegoś obawiać
  3. prędzej skorzystałbym tutaj z czegoś takiego jak Redis niż bawił się w back endzie w porównywanie co jest w bazie, a co front/API pisze do bazy
  4. czasem lepiej skorzystać z procesora serwera (back end w .NET/C#) niż zostawiać więcej operacji bazie

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