Zapis obiekty do bazy, XML i binarki

0

Cześć. Mam kilka obiektów (wszystkie dziedziczą po tej samej klasie, a czasem jeszcze po sobie) w kompozycie.

I teraz chcę móc je zapisywać na kilka sposobów:

  1. Do bazy danych (SQL i NoSQL)
  2. Do pliku XML
  3. Do binarki
    itd.

I od razu uprzedzam - jeśli chodzi o bazę danych to zapis ma być w odpowiednich tabelach i kolumnach, a nie coś w stylu XMLColumn :)

Ma być też opcja cofania operacji - tu prawdopodobnie skończy się na pamiątce.

I teraz tak. Mam kilka pomysłów, jak to zrobić i mam też własnego faworyta.
Bardziej mi jednak chodzi o sam sposób zapisu obiektu, niż o wzorzec w stylu CQRS.

No bo tak. Mógłbym stworzyć sobie jakiś interfejs do mapowania obiektu. Coś w stylu:

interface IMyClassMapper
{
    void Stream Map(MyClass object);
}

Dalej miałbym konkretne mappery: Do SQL, do XML, i do binarki. Wszystkie zwracałyby jakiś strumień albo w jakiś inny sposób odpowiednio sformatowane dane, dzięki którym wiedziałbym jak to zapisać.

Ale to tworzy pewien poważny problem. Skoro mam mieć zapis do kilku formatów, to muszę stworzyć kilka takich mapperów. A co za tym idzie, każdy mapper musiałby wiedzieć, jak zmapować obiekt. Czyli, jeśli w klasie dojdzie mi jakaś właściwość, to muszę ją dodawać w kilku mapperach. A to już problem.

Mógłbym w prawdzie użyć jednego mappera (właśnie teraz na to wpadłem ;)), który by zwracał coś w rodzaju słownika, gdzie kluczem byłaby nazwa pola, a wartością - wartość ;) Wtedy odpada mi problem jak wyżej. Następnie klasy do zapisu odpowiednio by sobie ten słownik obrabiały. Tak, jest to pewne rozwiązanie.Myślę, że tu też nie byłoby problemu, że zaimplementować pamiątkę. Problem jest za to inny - opisany niżej.

Mam też inny pomysł, który podpatrzyłem kiedyś w systemach Cadowych. W aplikacji, którą kiedyś tworzyłem świetnie się sprawdził. To coś w rodzaju aktywnego rekordu, ale nie do końca.

Chodzi o to, że każdy obiekt ma dwie metody w stylu:

bool DataIn(IObjectData data)
{
    base.DataIn(data);
    int version = data.Read("version");
    //tutaj możemy zareagować odpowiednio do wersji

    Name = data.Read("name");
}

bool DataOut(IObjectData data)
{
    base.DataOut(data);
    data.Write("version", 1);
    data.Write("name", Name);
}

Nie ukrywam, że pomysł podoba mi się bardziej. Obiekt nie zapisuje informacji o sobie nigdzie tak naprawdę, więc nie można powiedzieć, że łamie SRP. Można by się wprawdzie czepić, że obiekt pełni w pewien sposób rolę swojego mappera, ale z drugiej strony nie ma pojęcia o tym skąd odczytuje i gdzie zapisuje dane. Ja bym te dwie metody (DataIn i DataOut) traktował bardziej jako coś w rodzaju ToString. Tyle, że nie zwracamy stringa, a zapisujemy dane gdzieś indziej.

Jakie są zalety takiego rozwiązania? Przede wszystkim nie ma mappera. Tutaj klasa wie, jakie ma pola i jakie pola chce zapisywać. I od razu mogę korzystać z dziedziczenia. W momencie, gdy mamy taką sytuację:

class Abstract {}
class Concrete1: Abstract {}
class Concrete2: Abstract{}
class MyClass: Concrete2{}

to problem z Mapperami byłby taki, że musiałbym je dublować (mapper dla Concrete1 i mapper dla MyClass i mapper dla Concrete2, gdzie część pól byłaby taka sama) lub też tworzyć jakieś dziedziczenia mapperów (Mapper dla MyClass dziedziczyłby po Mapperze z Concrete2), co mi już tworzy dodatkową ilość klas dość sztucznie. No bo Mapper nie ma właściwie żadnej funkcjonalności poza tym, że przepisuje dane z jednego obiektu do drugiego. A tyle, ile obiektów do serializacji, tyle mapperów.

Przy rozwiązaniu z moim faworytem takiego problemu nie ma. Również nie ma żadnego kłopotu, żeby stosować pamiątkę. Pamiątka przechowuje obiekt IObjectData.

Więc uważam, że mój faworyt jest lepszym pomysłem niż mapper. Ale wiem, że jest to podobne do ActiveRecord, więc chciałbym zaprosić do dyskusji na temat tego rozwiązania :) Moim zdaniem jest dobre i nie łamie kluczowych zasad. Co o tym sądzicie?

1

Nie do końca rozumiem. Czy szukasz alternatyw projektowych, czy może jakiś konkretny problem masz do rozwiązania?

Na podstawie opisu ja bym to widział tak jak w załączniku.

  1. Obiekt dostaje jakiegoś mappera i sam decyduje, które z pól mają być mapperowi udostępnione.
  2. Mapper na podstawie wiedzy o polach decyduje, z których pól chce korzystać i w jaki sposób.
  3. Klasa abstrakcyjna wywołuje w mapMe operację doMapping() z mappera.
  4. Klasy potomne w mapMe() mogą dodawać coś mapperowi (via addField) i wołają mapowanie z nadklasy, tak by finalnie mapper wykonał swoją robotę.
    Do tego możesz mieć jakieś abstrakcyjne beforeMapping/afterMapping, tak by była możliwość zrobienia czegoś z podklasy (np. usunięcia pól nadklasy via removeField).

Jak dodasz nowe pole do którejś z klas to i tak nie wiadomo czy ono ma być udostępniane mapperom, gdzie zapisywane i w jaki sposób.

0

Możesz skorzystać z refleksji, wtedy nie będziesz musiał wprowadzać żadnych zmian do istniejącego kodu, kiedy dodasz (lub usuniesz) pole/właściwość. Coś na zasadzie:

interface IMapper
{
	Stream Map(object obj);
}

class OnlyReadWritePropertyMapper : IMapper
{
	public Stream Map(object obj)
	{
		if (obj == null)
			throw new ArgumentNullException(nameof(obj));

		var properties = obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(x => x.CanWrite && x.CanRead);
		foreach (var property in properties)
		{
			var value = property.GetValue(obj);
			// do sth...
		}

		// ...		
		return stream;
	}	
}
0
Juhas napisał(a):

Więc uważam, że mój faworyt jest lepszym pomysłem niż mapper. Ale wiem, że jest to podobne do ActiveRecord, więc chciałbym zaprosić do dyskusji na temat tego rozwiązania :) Moim zdaniem jest dobre i nie łamie kluczowych zasad. Co o tym sądzicie?

To nie jest podobne do ActiveRecord, bo ActiveRecord implementuje CRUDa na bazie danych. To co pokazałeś to właściwie Pamiątka i nie ma tu za bardzo co doszukiwać się problemów na siłę.

Większy problem jak widzę po dyskusji w komentarzach pod postem wyżej jest z implementacją. Moje dwa grosze:

  1. Refleksja służy właśnie do tego, aby w podobnie banalnych przypadkach ułatwiać sobie życie, a nie pisać małpiego kodu. O ile masz więcej niż 5 obiektów po 5 pól, to zrobienie generycznego rozwiązania się zwróci.
  2. Co do problemów z ewentualną refaktoryzacją - po pierwsze obsługa wyjątków, po drugie testy.
0

No to nie jest też Pamiątka (jako całość), ale do Pamiątki będzie użyte. Nie chcę tutaj refleksji, no bo ja to widzę tak (jakbym miał użyć refleksji): przelecieć się po wszystkich publicznych właściwościach i je zapisać. I tu pojawiają się już problemy. Jeśli mam mieć sensowny zapis do binarki, to muszę rozróżniać typy danych. W związku z tym potrzebuję metod w mapperze w stylu: WriteInt, WriteBool, WriteString. Owszem, mogę zrobić generycznego Write'a, ale prędzej czy później to się zemści - gdy np. dojdzie mi jakaś właściwość o typie nieobsługiwanym przez mappera. No i też widzę problem w odczycie takich danych. A jakbym miał wywoływać konkretną metodę pod typ konkretnej właściwości, no to to się mija z celem. Poza tym nie chcę zapisywać wszystkich publicznych właściwości. Prawdopodobnie też będę musiał zapisywać niektóre prywatne. I tu musiałbym się posiłkować atrybutami, których chcę uniknąć. Wydaje mi się, że będzie lepiej, gdy obiekt będzie decydował o tym, co ma zapisać.

0

Przy takich wymaganiach (zwłaszcza braku atrybutów), to jasne.
Ja z kolei raczej bym użył atrybutów i domyślnej serializacji, no ale ja jestem leniwy.

0

Testowałem domyślną serializację. Ale z tego, co czytałem to domyślna zapisuje tylko do plików. Nie obsługuje bazy danych (a przynajmniej nie ogarnąłem jak to zrobić). Chyba, że ją customizować. Poza tym mam jakieś takie wewnętrzne obawy, że domyślna serializacja może zmienić się w którejś wersji i pliki, które były zapisywane kiedyś, nie będą poprawnie odczytane. To są tylko moje obawy, bo pewnie to jest jakoś uniwersalnie zrobione. Ona generalnie działa bardzo fajnie, ale jak ją testowałem pod tym kątem, to wyszły mi jakieś problemy (nie pamiętam już co), które sprawiły, że do tego mojego zastosowania się nie nadaje. Może faktycznie chodziło o bazę danych... No nie pamiętam.

Ale jeśli zapis byłby tylko do pliku, to mechanizm naprawdę fajny. A czy gdzieś można znaleźć opisaną strukturę tej serializacji? W sensie strukturę, jaką tworzy w pliku.

1

Domyślnie w .NET masz serializatory binarny i XML, i strukturę w każdym z formatów możesz konfigurować odpowiednimi atrybutami. Co do bazy danych, to nie będzie to serializacja tylko mapowanie obiektów do relacji, a więc najprościej użyć ORMa.

0

Chodziło mi o strukturę całego pliku. Bo wiem, że mogę sobie wybrać jakie pola mają być zapisywane, a jakie nie. Ale chodzi mi o strukturę całego pliku. Tak z ciekawości.

0

W przypadku XML przecież strukturę widzisz, bo to w końcu XML, tu nie ma miejsca na psujące zmiany. Struktury binarnej nie ocenisz na oko, ale raczej marne szanse, żeby zmienili implementacje na niekompatybilną, skoro jest i działa od wielu lat.

0

Wiem, dlatego zaznaczyłem, że "pytam z ciekawości". Bo zauważyłem np., że chyba nie powielają stringów. Tzn. jeśli są dwa stringi z taką samą zawartością, to w pliku jest tylko jeden. Ale fakt, że gdzieś tam obawiam się, że kiedyś zmieni się coś w tej strukturze, co sprawi brak kompatybilności. Byłby to mocny strzał w kolano dla całego .NET, ale... historia zna podobne przypadki ;)

0

Witam,

mam pytanko : czy w opisywanych schemacie może zasinieć sytuacja
klasa b dziedziczy po klasie A, w klasie A zapisujemy publiczną właściwość A1 ale podczas zapisu klasy B nie ma być zapisywana publiczna właściwość A1

generalnie chodzi o to czy raz zdefiniowana właściwość/pole do zapisu będzie już zawsze zapisywana (w obiektach które z niej dziedziczą)?

0

Tak. Klasa bazowa musi jakoś swój stan zapamiętać - zapisać i odczytać. W innym przypadku może zostać niepoprawnie utworzona. Mam wrażenie, że próbujesz zrobić coś dziwnego. Ale jeśli z jakiegoś powodu nie chcesz zapisywać tego pola, to możesz uczynić je wirtualnym i przesłonić w klasie dziedziczącej.

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