Aktualizacja w bazie danych tylko zmienionych pól

0

Powiedzmy, że mam stronę WWW - typowego CRUDA z użytkownikami. Każdy użytkownik może edytować swój profil, czyli dane takie jak: nazwa, opis. Początkowo myślałem o tym, by aplikacja korzystała z kontekstu bazy danych z wykorzystaniem Entity Framework. Dzięki temu raz pobrane dane z bazy byłyby trzymane w ramie (a dokładniej w obiekcie kontekstu bazy danych), a potem entity framework dbałby o to, by przy aktualizacji modyfikowane były tylko te wartości, które uległy zmianie.

Jednakże korzystanie z kontekstu bazy danych w takim miejscu to nie jest najlepszy pomysł. Stwierdziłem jednak, że pomiędzy aplikacją WWW, a bazą danych będzie serwis, który będzie pośredniczył w wymianie informacji. Jako, że kontekst bazy danych teraz nie wychodzi poza serwis, to muszę jakoś inaczej dowiedzieć się, które elementy z profilu użytkownika uległy zmienie.

Macie jakiś sprawdzony sposób jak to zrobić? Dodam, że w moim przykładzie ten "profil użytkownika" będzie zupełnie czymś innym - wiele tabel, zagnieżdzeń itd.

0

A czy coś stoi na przeszkodzie, żeby poprosić serwis o odpytanie DB o konkretnego użytkownika i porównanie własności? Albo porównanie ich jeszcze w serwisie?

0

Ja mam na to sposób taki, że używam Automappera. Obiekt, który przychodzi do serwisu mapuję na wcześniej pobraną encję z bazy danych poprzez

Mapper.Map(obiekt_wejsciowy, encja);

Oczywiście wcześniej musisz utworzyć sobie profile mapowania uwzględniając zagnieżdżenia, o których mówisz, a przy pobieraniu encji z bazy musisz dociągnąć potrzebne property poprzez Include. Na koniec zapisujesz zmiany w kontekście. Pamiętać należy przy tym, aby nie używać metody Update, gdyż pobrany wcześniej obiekt i tak jest śledzony, a używając Update wymuszasz update wszystkich, nawet nie zmienionych pól.

1
west napisał(a):

Jednakże korzystanie z kontekstu bazy danych w takim miejscu to nie jest najlepszy pomysł. Stwierdziłem jednak, że pomiędzy aplikacją WWW, a bazą danych będzie serwis, który będzie pośredniczył w wymianie informacji. Jako, że kontekst bazy danych teraz nie wychodzi poza serwis, to muszę jakoś inaczej dowiedzieć się, które elementy z profilu użytkownika uległy zmienie.

Jak rozumiem, to aplikacja "wie", co w profilu zostało zmienione, tak? A więc niech serwis wystawia endpointy pozwalające na aktualizację poszczególnych części profilu. To, czy pod spodem będzie EF czy nie, to i tak nie ma znaczenia, bo z change trackingu i tak nie skorzystasz.

Macie jakiś sprawdzony sposób jak to zrobić? Dodam, że w moim przykładzie ten "profil użytkownika" będzie zupełnie czymś innym - wiele tabel, zagnieżdzeń itd.

Zazwyczaj, gdy w pytaniu tak naprawdę chodzi o "coś innego" okazuje się, że rozwiązywany jest problem X/Y.

0
somekind napisał(a):

Jak rozumiem, to aplikacja "wie", co w profilu zostało zmienione, tak? A więc niech serwis wystawia endpointy pozwalające na aktualizację poszczególnych części profilu. To, czy pod spodem będzie EF czy nie, to i tak nie ma znaczenia, bo z change trackingu i tak nie skorzystasz.

I do każdego takiego "pstryczka" wystawiać osobną metodę w serwisie? IMO słabe

Ja bym to widział tak, że w momencie, gdy użytkownik zatwierdza formularz, to do serwisu leci informacja tylko o zmienionych polach.

Postaram się to opisać na przykładzie:

Mam stronę z ustawieniami profilu użytkownika. Może on zmienić:

  1. Nazwę
  2. Opis
  3. Datę urodzin

Po zatwierdzeniu formularza do serwisu leci json z danymi, które użytkownik zmienił na froncie. Np. jeśli zmienił nazwę i zatwierdził formularz, to do serwisu poleci json:

{
	"name": "nowa-nazwa"
}

jeśli zmienił opis i datę urodzin, to do serwisu poleci:

{
	"desc": "nowy-opis",
	"birthday": "1990-09-08T19:01:55.714942+03:00"
}

Po zatwierdzeniu formularza, do serwisu trafia obiekt UserSettingsChangeDTO

public class UserSettingsChangeDTO
{
	public string Name {get; set;}
	public string Description {get; set;}
	public DateTime? Birthday {get; set;}
}

I w zależności od tego, które pola mają ustawioną wartość, to wykonuję odpowiednie aktualizacje w bazie danych.

Co jeszcze moim zdaniem możnaby dodać, to przesyłać do serwisu informacje o aktualnych (sprzed zmiany) ustawieniach. Dzięki temu, w sytuacji, gdy 2 osoby próbowałyby na raz zmienić jakieś ustawienia, to serwis uniemożliwiłby to w razie "konfliktu". Chodzi o sytuację:

Ustawienia początkowe:
number: 1

  1. Użytkownik A przechodzi do ustawień i widzi aktualną wartość number: 1
  2. Użytkownik B przechodzi do ustawień i widzi aktualną wartość number: 1
  3. Użytkownik A zmienia number na 2
  4. Użytkownik B zmienia number na 3
  5. Użytkownik A zatwierdza formularz i wysyła do serwisu poprzednią wczytaną wartość number: 1, oraz nową: 2
  6. Serwis pozwala na zmianę wartości number na: 2
  7. Użytkownik B zatwierdza formularz i wysyła do serwisu poprzednią wczytaną wartość number: 1, oraz nową: 3
  8. Serwis nie pozwala na zmianę wartości number na: 3, ponieważ użytkownik przesłał złą poprzednią wartość (ktoś zdążył ją zmienić po tym, jak ją odczytał)

Takie coś miałoby sens, gdyby wielu użytkowników miało dostęp do jakiegoś panelu z ustawieniami i nie chcielibyśmy, by ktoś nadpisał czyjeś zmiany.

Chyba też wiem, gdzie leży główny problem.. W normalnych systemach aktualizacje są małymi modułami - aktualizuj post, aktualizuj profil, aktualizuj hasło itd. To wszystko da się wyciągnąć do osobnych metod serwisu. W moim przypadku to musi być 1 metoda, która obsługuje aktualizacje w wielu miejsach, coś w stylu: najpierw zmieniamy treści posta, potem hasło, potem zdjęcia, a na końcu wciskamy przycisk i te wszystkie zmiany muszą być wprowadzone 1 żądaniem - coś jak z gitem - najpierw wprowadzamy zmiany w wielu miejsach, a potem 1 przyciskiem je zatwierdzamy.

2
west napisał(a):

I do każdego takiego "pstryczka" wystawiać osobną metodę w serwisie? IMO słabe

Nie pisałem o każdym polu tylko o części profilu. Np. jedna metoda do danych adresowych, druga do kart kredytowych, a trzecia do konfiguracji powiadomień.

Po zatwierdzeniu formularza, do serwisu trafia obiekt UserSettingsChangeDTO

public class UserSettingsChangeDTO
{
	public string Name {get; set;}
	public string Description {get; set;}
	public DateTime? Birthday {get; set;}
}

I w zależności od tego, które pola mają ustawioną wartość, to wykonuję odpowiednie aktualizacje w bazie danych.

Wszystkie pola będą zawsze obowiązkowe? Bo jeśli nie, to nie pozwolisz w ten sposób wyzerować/wyczyścić żadnej wartości.

Co tak naprawdę chcesz zoptymalizować w ten sposób? Ruch sieciowy? Operacje na bazie?
Zmierzyłeś, że któreś z tych jest problemem, a ta optymalizacja to naprawi?

Co jeszcze moim zdaniem możnaby dodać, to przesyłać do serwisu informacje o aktualnych (sprzed zmiany) ustawieniach. Dzięki temu, w sytuacji, gdy 2 osoby próbowałyby na raz zmienić jakieś ustawienia, to serwis uniemożliwiłby to w razie "konfliktu".

Skoro za każdym razem chcesz wysłać "tylko zmienione pola" + ich oryginalne wartości (a nie np. timestamp rekordu), to nie chodzi Ci o optymalizację ruchu sieciowego, bo wyślesz więcej niż trzeba.

Chyba też wiem, gdzie leży główny problem.. W normalnych systemach aktualizacje są małymi modułami - aktualizuj post, aktualizuj profil, aktualizuj hasło itd. To wszystko da się wyciągnąć do osobnych metod serwisu. W moim przypadku to musi być 1 metoda, która obsługuje aktualizacje w wielu miejsach, coś w stylu: najpierw zmieniamy treści posta, potem hasło, potem zdjęcia, a na końcu wciskamy przycisk i te wszystkie zmiany muszą być wprowadzone 1 żądaniem

To nie jest nic nienormalnego, to jest powszechna potrzeba.

0

Co jeszcze moim zdaniem możnaby dodać, to przesyłać do serwisu informacje o aktualnych (sprzed zmiany) ustawieniach. Dzięki temu, w sytuacji, gdy 2 osoby próbowałyby na raz zmienić jakieś ustawienia, to serwis uniemożliwiłby to w razie "konfliktu". Chodzi o sytuację:

czy przypadkiem do takich rzeczy nie stosuje się takich rozwiązań jak rowversion czyli po prostu wersja wiersza?

Id | Name | RowVersion
1 | 'xD' | 1

I do każdego takiego "pstryczka" wystawiać osobną metodę w serwisie? IMO słabe

Jeżeli chodzi o checkboxy, to czy do czegoś takiego nie stosuje się czego typu permission bits?

screenshot-20210104171016.png

0
somekind napisał(a):

Nie pisałem o każdym polu tylko o części profilu. Np. jedna metoda do danych adresowych, druga do kart kredytowych, a trzecia do konfiguracji powiadomień.

Ale ja mam te wszystkie 3 rzeczy na 1 widoku i chciałbym, by po zatwierdzeniu formularza szło to do serwisu jako 1 żądanie aktualizacji danych. Po co robić z tego 3 różne metody? Odnosząc się do przykładu forum internetowego - użytkownik może zedytować dowolony swój post, nazwę, datę urodzenia itd., ale dopiero po kliknięciu zatwierdź, informacja ta leci do serwisu i są wprowadzane odpowiednie zmiany w bazie danych. W większości przypadków mielibyśmy osobne metody - aktualizacja postu, aktualizacja nazwy, aktualizacja daty urodzenia, dostępne w zupełnie innych widokach, ale to nie jest coś, co ja chcę zrobić. Najpierw na froncie wprowadzamy zmiany i one zapisują się niczym produkty w koszyku w sklepie internetowym, a potem, możemy je przesłać do serwisu jednym kliknięciem i zaktualizować w bazie tylko co, co było zmienione.

somekind napisał(a):

Wszystkie pola będą zawsze obowiązkowe? Bo jeśli nie, to nie pozwolisz w ten sposób wyzerować/wyczyścić żadnej wartości.

Co tak naprawdę chcesz zoptymalizować w ten sposób? Ruch sieciowy? Operacje na bazie?
Zmierzyłeś, że któreś z tych jest problemem, a ta optymalizacja to naprawi?

Chcę zmniejszyć ilość operacji na bazie danych, a dokładniej - nie aktualizować tego, co nie jest potrzebne. Użytkownik zaktualizował swoją nazwę użytkownika i treść postu nr 12612, to chcę tylko te dane przesłać do serwisu i tylko te dane zaktualizować w bazie danych.

somekind napisał(a):

Skoro za każdym razem chcesz wysłać "tylko zmienione pola" + ich oryginalne wartości (a nie np. timestamp rekordu), to nie chodzi Ci o optymalizację ruchu sieciowego, bo wyślesz więcej niż trzeba.

Nie pisałem o optymalizacji ruchu sieciowego, a o nieprzesyłaniu nadmiarowych danych.

WeiXiao napisał(a):

czy przypadkiem do takich rzeczy nie stosuje się takich rozwiązań jak rowversion czyli po prostu wersja wiersza?

Nie jestem pewien czy to rozwiązuje mój problem. Mi chodziło tylko o to, by jakieś dane nie zostały nadpisane, gdy 2 osoby na raz zaktualizowały np. treść posta.

WeiXiao napisał(a):

Jeżeli chodzi o checkboxy, to czy do czegoś takiego nie stosuje się czego typu permission bits?

Do tego może i tak (chociaż i tak bym tego nie użył), bo praca z takimi enigmatycznymi cyferkami jest do d**y. Tak czy siak, w moim przypadku nie będzie samych checkboxów, a więcej innych rzeczy (patrz wyżej przykład forum).

0
west napisał(a):

Ale ja mam te wszystkie 3 rzeczy na 1 widoku i chciałbym, by po zatwierdzeniu formularza szło to do serwisu jako 1 żądanie aktualizacji danych. Po co robić z tego 3 różne metody?

Zgadywałem po prostu, bo napisałeś, że nie chcesz przesyłać za dużo, więc podzielenie na kilka oddzielnych operacji wydawało się intuicyjne.

Najpierw na froncie wprowadzamy zmiany i one zapisują się niczym produkty w koszyku w sklepie internetowym, a potem, możemy je przesłać do serwisu jednym kliknięciem i zaktualizować w bazie tylko co, co było zmienione.

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

Chcę zmniejszyć ilość operacji na bazie danych, a dokładniej - nie aktualizować tego, co nie jest potrzebne. Użytkownik zaktualizował swoją nazwę użytkownika i treść postu nr 12612, to chcę tylko te dane przesłać do serwisu i tylko te dane zaktualizować w bazie danych.

A jak wygląda struktura danych w bazie? Bo moim zdaniem, dla bazy nie ma większego znaczenia, czy robisz update na jednej czy na 50 kolumnach tego samego wiersza.

Nie pisałem o optymalizacji ruchu sieciowego, a o nieprzesyłaniu nadmiarowych danych.

Jeśli przesyłasz mniej danych, to optymalizujesz zużycie sieci. :)

Nie jestem pewien czy to rozwiązuje mój problem. Mi chodziło tylko o to, by jakieś dane nie zostały nadpisane, gdy 2 osoby na raz zaktualizowały np. treść posta.

Tak, optymistyczna kontrola współbieżności (optiimistc concurrency control) opisana przez @WeiXiao rozwiązuje problemy ze współbieżnością, które opisujesz.
Twój pierwotny pomysł to też była optymistyczna kontrola, tylko zamiast sprawdzania jednej wersji rekordu chcesz sprawdzać wszystkie wartości. Jakby więcej roboty, jakby nieco trudniej.

0

ifek tu, ifek tam

a po prostu tak?

public class UserSettingsFacade
{
	private readonly UserNotificationService _userNotificationService { get; set; }
	
	private readonly UserEmailService _userEmailService { get; set; }
	
	private readonly UserSettingsInternal_XD_Service _settingsService { get; set; }
	
	public UserSettingsFacade(UserNotificationService userNotificationService, UserEmailService userEmailService, UserSettingsInternal_XD_Service xD)
	{
		_userNotificationService = userNotificationService;
		_userEmailService = userEmailService;
		_settingsService = xD;
	}
	
	public blabla ChangeUserSettings(User user, SettingsInput data)
	{
		if (!string.IsNullOrEmpty(data.Name))
		{
			_settingsService.ChangeUserName(user, data.Name);
		}
		
		if (!string.IsNullOrEmpty(data.Email))
		{
			_userEmailService.ChangeEmail(user, data.Email);
		}
		
		if (!string.IsNullOrEmpty(data.NotificationCosTam))
		{
			_userNotificationService.ChangeCosTam(user, data.NotificationCosTam);
		}
		
		if (data.Birthday.HasValue)
		{
			_settingsService.ChangeBirthday(user, data.Birthday);
		}		

		await context.SaveChangesAsync()
	}
}

no tylko gorzej gdy będziesz chciał na NULLa ustawić, no ale to z drugiej strony mógłbyś równie dobrze w jsonie wysyłać słownik i robić na zasadzie ContainsKey

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/questions/41482484/ef-automapper-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

@west: No spoko, ale mam kilka uwag:

  1. Czemu to robisz w .NET Framework 4.5.2 a nie w czymś nowszym chociażby .NET 5?
  2. Masz tutaj tylko dwie biblioteki, więc nie wiem gdzie je wykorzystujesz, ale pomyślałbym nad rejestracją DbContextu w kontenerze i wstrzykiwania go do serwisu.
  3. Dlaczego wyłączasz LazyLoading? Jak coś pobierasz z bazy to praktycznie wszystkie pola z danej tabeli są pobierane, (lazy loading działa na navigation properties) np. jeżeli masz property w Userze załóżmy Orders, gdzie jest lista zamówień to wtedy musisz zrobić Include, żeby je pobrać. Poza tym pobierając dane jeśli chcesz je zmapować na jakiś typ do wyjścia to po prostu używasz ProjectTo z Automappera, i nawet jeśli masz model wyjściowy zawierający użytkowników z zamówieniami to nie musisz wtedy pamiętać o Include, bo AutoMapper sobie to ogarnie :P
  4. No i inicjalizacja AutoMappera, jak projekt ma być taki prosty jak ten to ok, ale jeśli będziesz mieć duży projekt z wieloma modelami i mapowania nie będą tak proste jak u Ciebie to pomyśl jak będzie wyglądała ta metoda. Kierowałbym się tutaj w stronę użycia profili mapowania: https://docs.automapper.org/en/stable/Configuration.html#profile-instances i potem tylko przy inicjalizacji automappera podajesz assembly, w którym ma szukać profili i sam sobie je rejestruje.

@pikob Ad1: jeśli pobierzesz encję z bazy ze śledzeniem i zmienisz jej wartość jakiegoś property, następnie odpalisz SaveChanges to wejdź w profilera - update poleci tylko na tym polu. Ale jeśli przed SaveChanges wywołasz Update to wtedy zrobi update na wszystkich polach bez względu na to czy są zmieniane. Przynajmniej jeśli chodzi o EF Core.
Ad2: to nie jest nic nadzwyczajnego mieć miliony rekordów w tabelach.
Ad4: z tym się nie zgodzę, jeśli jest jakaś operacja na dużym zbiorze danych to wolę skorzystać albo z jakiegoś BulkUpdate lub wywołać RawSql niż robić to po stronie C# bo czasami oszczędności chociażby czasu są bardzo duże. Faktycznie tracę na tym jakąś małą możliwość debugowania, ale zyskuję bardzo dużo na wydajności.

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