Edycja obiektu

0

Witam

Prosiłbym o pomoc w rozwiązaniu pewnego problemu. Posiadam dosyć rozbudowany obiekt (tu pokaże tylko fragment) np. Person w skład którego wchodzą m.in. lista obiektów typu Address. Każdy obiekt w moim programie wywodzi się z klasy bazowej Entity, która ma m.in. flagę IsChanged

    public bool IsChanged
    {
        set
        {
            this.m_isChanged = value;
        }
        get
        {
            return m_isChanged;
        }
    }

nadając wartości poszczególnym właściwościom obiektów Person i Address postępuje tak:

    public string PESEL
    {
        set
        {
            this._PESEL = value;
            m_isChanged |= (_PESEL != value);
        }
        get
        {
            return this._PESEL;
        }
    }

tak więc zmienia się flaga isChanged.

Programuję używając modelu MVP.
Na moim widoku (UI) mam TabControl i kilkanaście zakładek gdzie ładowany jest cały obiekt person. Na pierwszej zakładce są podstawowe dane obiektu Person, na kolejnych zakładkach mamy np. grida z listą Adresów. Tutaj możemy dodawać/usuwać lub edytować poszczególne adresy klikając na buttony (add/mod/del) które wywołują kolejny widok (UI) do edycji adresu.

Jak dokonać zapisu do bazy danych:
a) nowego
b) edytowanego

obiektu Person z listą n-adresów? Za komunikację z bazą odpowiada u mnie warstwa DAL (serwisy).
Problem widzę np.

  • w edycji wcześniej utworzonego obiektu. Jak wykryć i sprawnie dokonać zapisu tylko tych property które były edytowane? Jak usunąć tylko te adresy (relacje w bazie) które zostały usunięte z grida?
    Chciałbym też aby przycisk Zapis był enable tylko wtedy gdy naprawdę coś się zmieniło w obiekcie Person a nie w sytuacji gdy obiekt ten był np. edytowany (zmiana jednej z właściwości potem ponowna zmiana tej samej właściwości na pierwotną wartość).
    Domyślam się że muszę tu sprawdzać porównać dwa obiekty przed edycją i po edycji. Wykonac DeepCopy edytowanego obiektu. Nie za bardzo wiem jak to zrobić i jak do tego optymalnie podejść. czy mógłby ktoś mnie podprowadzić ?

Proszę o pomoc bo trochę mnie to przerasta.

0

Jeśli chodzi o głęboką kopię, to w miarę łatwym w implementacji sposobem jest serializacja binarna.
A co do zapisu do bazy danych, to zależy jakiej technologi używasz do jej obsługi, a tego nie napisałeś.

0

ad1. Tak trochę czytałem o tym m.in. tu:
http://www.csharp411.com/c-object-clone-wars/
jednak jakoś tego nie widzę w moich obiektach, gdy składają się z kolekcji a czasem z wielu kolekcji posiadających inne kolekcje (IList). Poza tym jak sprawnie porównywać takie obiekty - Właściwość po właściwości, tak jak przeszukiwanie drzewa??
Wcześniej nie wspomniałem jeszcze o problemie walidacji która jest przeprowadzana w dwóch warstwach - walidacja po stronie aplikacji, a następnie w najniższej warstwie po stronie DB. Problem widzę też np. gdy zostanie zwrócony błąd walidacji po stronie DB przy jednej z właściwości w którejś kolekcji - rozwiązaniem są domyślam się transakcje uruchamiane i kończone po stronie aplikacji - jednak i tu boję się tzw. loock'ów przy błędach krytycznych aplikacji.

ad2. Korzystam z ADO.net, Model połączeniowy - za każdym razem zestawiam połączenie z bazą. Po stronie DB mam tylko procedury,a po stronie aplikacji występuje jeden moduł odpowiedzialny za komunikację z bazą. Moduł ten to seria serwisów (CAB/SCSF Framework) ładowanych do WorkItemów z których to mogę skorzystać w dowolnym miejscu aplikacji. (zapisy i odczyty odbywają się przez wywołanie ExecuteNonQuery lub Reader...)

  • nie jestem zainteresowany NHibernate... czy LINQ....i innymi podobnymi...
0
karlosss napisał(a)

Poza tym jak sprawnie porównywać takie obiekty - Właściwość po właściwości, tak jak przeszukiwanie drzewa??

Generalnie tak, powinno się nadpisać metodę Equals w każdej klasie tak, aby porównywała wszystkie właściwości obiektu oraz rekurencyjnie wywoływała Equals wszystkich swoich podobiektów. Dlatego wspomniałem o użyciu serializacji, która trochę to ułatwia, bo po zserializowaniu obiektu wystarczy porównać tablicę bajtów. Ale to pozwala sprawdzić czy cały obiekt się zmienił.
Skoro chcesz zapisać tylko zmienione wartości, to tak czy siak musisz przejść przez cały graf obiektu, sprawdzić co się zmieniło i zbudować dynamicznie zapytanie. Szczerze mówiąc, nigdy takiego rozwiązania nie widziałem.

Problem widzę też np. gdy zostanie zwrócony błąd walidacji po stronie DB przy jednej z właściwości w którejś kolekcji - rozwiązaniem są domyślam się transakcje uruchamiane i kończone po stronie aplikacji - jednak i tu boję się tzw. loock'ów przy błędach krytycznych aplikacji.

Jakiej walidacji po stronie DB? O co dokładnie Ci chodzi?

nie jestem zainteresowany NHibernate... czy LINQ....i innymi podobnymi...

W ten sposób dodajesz sobie chyba tylko dodatkowej pracy.

0

ad1. Budowa dynamicznych zapytań SQL'owych odpada - m.in. z powodu założeń projektu. poza tym aplikacja ma być na tyle łatwa w serwisowaniu że w razie problemów wystarczy wprowadzać zmiany w procedurach DB bez potrzeby rekompilacji. Tak to aplikacja klasy ERP. Maksimum możliwości serwisowych, wsparcia klienta no i SKALOWNOŚĆ.
A nie da się zaimplementować takiej metody Equal lub Compare w klasie bazowej Entity - w końcu każda klasa wywodzi się z niej. Tylko jak to się ma w przypadku:

Person : Entity
Address : Entity
Person.Address -> tu wychodzi że możemy skorzystać z dwóch takich metod porównawczych. :( Bałagan :(

ad2. Jako że aplikacja działa w modelu połączeniowym muszę za każdym razem walidować dane, które chcę zapisać - zarówno po stronie aplikacji jak i po stronie DB. Przypadki które muszę wyeliminować
Przykład 1:
baza danych została zaktualizowana, a jedna z końcówek nie została zamknięta. Końcówka ma stary exe (sprawdzanie wersji bazy i aplikacji odbywa się tylko przy uruchamianiu - performance). Końcówka będzie działać ale nie koniecznie wg. aktualnych założeń. Dlatego muszę walidować dane przed zapisem dodatkowo po stronie DB. Po stronie aplikacji występuje walidacja, aby zapewnić wrażenie większej integracji programu z użytkownikiem i aby nie generować zbędnego ruchu aplikacja-baza.
Przykład 2:
Jeśli widziałeś program CDN XL, Optima... jest tam możliwość wyboru danych ze słownika. Podobnie u mnie. Mam formę adresu, wybieram miejscowość ze słownika... czekam minutę inny użytkownik usuwa miejscowość ze słownika, a następnie ja próbuję zapisać adres do bazy. Po stronie aplikacji udało się nam pobrać odpowiednie dane i są w obiekcie dlatego dodatkowo po stronie bazy sprawdzam czy wartość słownikowa jeszcze tam występuje.
I jeszcze inne podobne przykłady......

ad3.
Nie stać mnie na naukę i implementację nowego framewodku. Poza tym musiałbym dużo już zmieniać a to kosztowne (czas).

0

Tutaj możesz zobaczyć część Widoków (UI) jakie już "prawie" działają i gadają z bazą oraz charakter aplikacji.
http://showonline.wordpress.com/2011/03/12/katalog-kontrahentw/
Może będzie łatwiej Ci dzięki temu zobrazować moje nie zawsze jasne tłumaczenia :)

Przydałyby mi się jakakolwiek pomoc. Jestem w takiej sytuacji że przez moment zastanawiałem się nad zmianą modelu połączenia (na bezpołączeniowy) z bazą danych. Wtedy wiele moich problemów by nie istniało. Ale chciałbym zostać przy modelu połączeniowym i zachować przywileje modelu bezpołączeniowego :) (m.in. jak najbardziej intuicyjna, prosta i szybka praca za aplikacją dla użytkownika końcowego). Jak najmniej klikania "ZAPISZ". Chciałbym aby klikało się zapisz dla "całego obiektu" -> Osoba + adresy + dokumenty + inne kolekcje. Czy trochę rozjaśniłem problem :) ?

0

Co do porównywania obiektów - refleksja, jedziesz w pętli po właściwościach, sprawdzasz, które się zmieniły i budujesz dynamicznie zapytanie. Inaczej nie osiągniesz tego co chcesz - nawet istniejące ORMy raczej nie robią selektywnych update'ów. Jak inaczej chcesz wykonać zapytanie nie tworząc go wcześniej?

ad 2 p1: Podwójna walidacja to zło, komplikacja, zmniejszenie wydajności, itd. Niemniej jednak skoro tak już się dzieje, to chyba rzucasz z procedury jakiegoś RAISEERROR, prawda? Aplikacja powinna go przechwycić. Chociaż to jest dość sztuczny problem, należałoby raczej zadbać aby aktualizacja schematu bazy danych zawsze wymagała nowej wersji aplikacji.
ad 2p2: W takiej będzie błąd naruszenia integralności referencyjnej z bazy danych. To też przechwytujesz, odświeżasz użytkownikowi interfejs, itd.

Ty chyba oczekujesz cudu. Skoro nie chcesz wykorzystać ORMa, który zrobiłby całą magię za Ciebie, to musisz niestety zaimplementować tę magię sam. Założę się, że to pierwsze byłoby łatwiejsze i tańsze niż to drugie.

0

Małe sprostowanie. Nie chcę zapisywać (update) pojedynczych właściwości np. Nazwisko lub sama Miejscowość. Chodziło mi raczej o grupę właściwości, wręcz "składowe" obiekty typu Adres składające się na obiekt Person.

Ad 2p1 - Definitywnie nie zgodzę się z Tobą jakoby podwójna walidacja (w dwóch warstwach) była złem. Wręcz przeciwnie. Walidacja w aplikacji to odciążenie (sito) serwera BD. Nie ma sensu zawracać głowy silnikowi bazy danych i generować niepotrzebny ruch pomiędzy serwerem i aplikacją. Komplikacja? Nie koniecznie. Ważne jest aby w warstwie DB walidatory były szczelne. jak zapomni się o czymś w aplikacji to DB zwróci info. Gdzie widzisz problem wydajności skoro to aplikacja w modelu RichClient?

  • Tak mam coś takiego jak RAISEERROR :)

Ok może trochę ważniejszego kodu wkleję. Na początek z bazy danych. Nie pytaj dlaczego taka a nie inna koncepcja i budowa tabel oraz procedur. Takie założenia a opis i wytłumaczenie tego zajęłoby wieki :)

na przykładzie słownika krajów:

 
-- ****************************************************************************
-- Procedura: sp_InsCountry
-- ****************************************************************************
--
-- Wstawienie nowego rekordu do tabeli country
-- oraz w razie potrzeby zaktualizowanie poprzednika
-- Procedura zwraca kod 0 lub inny oraz IDR nowo wstawionego rekordu

IF EXISTS (SELECT 1 FROM [sys].[procedures] WHERE [name] = N'sp_InsCountry')
    DROP PROCEDURE [pap].[sp_InsCountry]
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE [pap].[sp_InsCountry]
    @p_current_user [pap].[idr],
    @p_ido [pap].[ido],
    @p_name nvarchar(100),
    @p_code2 nvarchar(2),
    @p_code3 nvarchar(3),
    @p_nationality nvarchar(100),
    @p_ue bit,
    @p_status tinyint,
    @p_system bit,
    @p_new_idr [pap].[idr] OUTPUT
WITH ENCRYPTION
AS
DECLARE -- Zmienne pomocnicze
    @ErrorCode int,
    @ErrorMsg nvarchar(100),
    @OldIdr [pap].[idr],
    @Log nvarchar(1000)

    SET NOCOUNT ON
    BEGIN TRANSACTION INSERT_COUNTRY
    
    --	Inicjalizacja zmiennych
    SET @ErrorCode = 0;
    SET @ErrorMsg = N'';
    SET @OldIdr = 0;
    
    -- Pobieranie nowego IDR'a
    EXEC @ErrorCode = [pap].[sp_GenIdr] @p_current_user, @p_ido, @p_new_idr OUTPUT
    IF (@ErrorCode <> 0)
        BEGIN
            SET @ErrorCode = 1
            SET @ErrorMsg = N'Błąd generowania identyfikatora rekordu';
            GOTO Quit
        END

    -- Przechowanie Idr'a (później będzie on wykorzystany do usunięcia rekordu z widoku)
    -- Domyślnie OldIdr = 0. Jeśli wstawiamy nowy rekord to OldIdr będzie dalej = 0
    -- W przeciwnym wypadku w zmiennej OldIdr będzie Idr rekordu który chcemy zmienić.
    SELECT TOP 1 @OldIdr = [idr] FROM [pap].[act_available_country] 
        WHERE [ido] = @p_ido
    
    --IF (@OldIdr IS NULL)
    --    BEGIN
    --        SET @ErrorCode = 2;
    --        SET @ErrorMsg = N'Brak rekordu w widoku';
    --        GOTO Quit
    --    END

    BEGIN TRY
    -- Chwilowe usunięcie rekordu z widoku act_country aby 
    -- nie naruszyć PK, FK oraz wstawić nowy zaktualizowany rekord do tabeli
    -- Operacja wykona się jeśli dokonujemy zmiany istniejących danych
    IF (@OldIdr <> 0)
    UPDATE [pap].[country] SET [next_idr] = @OldIdr
        WHERE [idr] = @OldIdr

    INSERT INTO [pap].[act_country] (
        [idr],
        [ido],
        [name],
        [code2],
        [code3],
        [nationality],
        [ue],
        [status],
        [system]
    )
    VALUES ( 
        @p_new_idr,
        @p_ido,
        @p_name,
        @p_code2,
        @p_code3,
        @p_nationality,
        @p_ue,
        @p_status,
        @p_system
    )

    -- Korekta (widok)
    IF (@OldIdr <> 0)
    UPDATE [pap].[country] SET [next_idr] = @p_new_idr
        WHERE [idr] = @OldIdr
        
    END TRY
    BEGIN CATCH 
            SET @ErrorCode = 9
            SET @ErrorMsg = N'Błąd zapisu.';
            GOTO Quit
    END CATCH
        
    SET NOCOUNT OFF
    
Quit:
IF (@ErrorCode <> 0)
    BEGIN
        ROLLBACK TRANSACTION INSERT_COUNTRY
        SET @Log = @ErrorMsg + N' (' + @p_name + N')' 
        EXEC [pap].[sp_WriteLog] @p_current_user, 2, N'sp_InsCountry', @Log
        RETURN @ErrorCode
    END
ELSE
    BEGIN
        COMMIT TRANSACTION INSERT_COUNTRY
        RETURN 0
    END
GO

i procedura do dodania nowej pozycji do słownika

 
-- ****************************************************************************
-- Procedura: sp_AddCountry
-- ****************************************************************************
--
-- Dodanie nowej pozycji (kraju)
-- Procedura zwraca kod 0 lub inny

IF EXISTS (SELECT 1 FROM [sys].[procedures] WHERE [name] = N'sp_AddCountry')
    DROP PROCEDURE [pap].[sp_AddCountry]
GO

CREATE PROCEDURE [pap].[sp_AddCountry]
    @current_user [pap].[login],
    @name nvarchar(100),
    @code2 nvarchar(2),
    @code3 nvarchar(3),
    @nationality nvarchar(100),
    @ue bit,
    @status tinyint
WITH ENCRYPTION
AS
DECLARE -- Zmienne pomocnicze
    @ErrorCode int, 
    @ErrorMsg nvarchar(100),
    @NewIdr [pap].[idr],
    @NewIdo [pap].[ido],
    @Log nvarchar(1000)

    SET NOCOUNT ON

    BEGIN TRANSACTION ADD_COUNTRY
	--	Inicjalizacja zmiennych
    SET @ErrorCode = 0;
    SET @ErrorMsg = N'';

    -- Sprawdzanie uprawnień
    EXEC @ErrorCode = [pap].[sp_CheckUserRight] @current_user, N'PAP', N'ADD_COUNTRY'
    IF (@ErrorCode <> 0)
        BEGIN
            SET @ErrorCode = 1;
            SET @ErrorMsg = N'Brak uprawnień';
            GOTO Quit
        END

    -- Sprawdzanie czy kraj już taki istnieje (nazwa) w aktualnie dostępnej liście
    IF EXISTS(SELECT 1 FROM [pap].[act_available_country] WHERE UPPER([name]) = UPPER(@name))
        BEGIN
            SET @ErrorCode = 6
            SET @ErrorMsg = N'Wybrana nazwa kraju istnieje już w bazie danych';
            GOTO Quit
        END
        
    -- Sprawdzanie czy kod2 kraju już taki istnieje (kod występuje ale w innej pozycji (ido))
    IF EXISTS(SELECT 1 FROM [pap].[act_available_country] WHERE [code2] = @code2)
        BEGIN
            SET @ErrorCode = 6
            SET @ErrorMsg = N'Wybrany kod (2 znaki) kraju istnieje już w bazie danych';
            GOTO Quit
        END

    -- Sprawdzanie czy kod3 kraju już taki istnieje (kod występuje ale w innej pozycji (ido))
    IF EXISTS(SELECT 1 FROM [pap].[act_available_country] WHERE [code3] = @code3)
        BEGIN
            SET @ErrorCode = 6
            SET @ErrorMsg = N'Wybrany kod (3 znaki) kraju istnieje już w bazie danych';
            GOTO Quit
        END
        
    -- Sprawdzanie nazwy kraju (po usunięciu spacji)
    IF (RTRIM(LTRIM(@name)) = '')
        BEGIN
            SET @ErrorCode = 7;
            SET @ErrorMsg = N'Brak nazwy kraju (nazwa)';
            GOTO Quit
        END
		
    -- Sprawdzanie kodu2 kraju (po usunięciu spacji)
    IF (RTRIM(LTRIM(@code2)) = '')
        BEGIN
            SET @ErrorCode = 11;
            SET @ErrorMsg = N'Brak kodu kraju (2 znaki)';
            GOTO Quit
        END

    -- Sprawdzanie kodu3 kraju (po usunięciu spacji)
    IF (RTRIM(LTRIM(@code3)) = '')
        BEGIN
            SET @ErrorCode = 11;
            SET @ErrorMsg = N'Brak kodu kraju (3 znaki)';
            GOTO Quit
        END
                		
    -- Pobieranie kolejnego wolnego numeru IDO i sprawdzenie czy nie przekroczyliśmy
    -- maksymalnej liczby pozycji w slowniku systemu.
    SELECT @NewIdo = MAX([ido]) FROM [pap].[act_country]
    IF (@NewIdo IS NOT NULL)
        IF (@NewIdo >= 99999999)
            BEGIN
                SET @ErrorCode = 2
                SET @ErrorMsg = N'Przekroczona maksymalna liczba pozycji w słowniku krajów'
                GOTO Quit
            END
        ELSE
            SET @NewIdo = @NewIdo + 1
    ELSE
        SET @NewIdo = 0 

    -- Wstawienie rekordu do tabeli kraj
    EXEC @ErrorCode = [pap].[sp_InsCountry] 
        @p_current_user = @current_user,
        @p_ido = @NewIdo,
        @p_name = @name,
        @p_code2 = @code2,
        @p_code3 = @code3,
        @p_nationality = @nationality,
        @p_ue = @ue,
        @p_status = @status,
        @p_system = 0,
        @p_new_idr = @NewIdr OUTPUT
        
    IF (@ErrorCode <> 0)
        BEGIN
            SET @ErrorCode = 3
            SET @ErrorMsg = N'Błąd wstawiania nowego rekordu słownika krajów';
            GOTO Quit
        END


    SET NOCOUNT OFF

Quit:
IF (@ErrorCode <> 0)
    BEGIN
        ROLLBACK TRANSACTION ADD_COUNTRY
        SET @Log = @ErrorMsg + N' (' + @name + N')'
        EXEC [pap].[sp_WriteLog] @current_user, 2, N'sp_AddCountry', @Log
        RAISERROR(@ErrorMsg, 16, 1)
        RETURN @ErrorCode
    END
ELSE
    BEGIN
		COMMIT TRANSACTION ADD_COUNTRY
		SET @Log = N'Dodanie kraju: (' + @name + N')'
		EXEC [pap].[sp_WriteLog] @current_user, 0, N'sp_AddCountry', @Log
        RETURN 0
    END
GO

To tylko poglądowe przykłady. I nie są jeszcze w 100% optymalne etc. Mogą się wydać Ci bardzo nietypowe i nieoptymalne, ale wierz mi są bardzo przemyślane

Chociaż to jest dość sztuczny problem, należałoby raczej zadbać aby aktualizacja schematu bazy danych zawsze wymagała nowej wersji aplikacji.

No i jest to sprawdzane podczas uruchamiania aplikacji. Nie ma sensu sprawdzać tego w każdej procedurze bo to dopiero negatywnie wpłynęłoby na wydajność systemu.
Niemniej jednak są takie przypadki że ktoś nie zamknie jednej z końcówek, a dodatkowo ta końcówka nie zostanie zaktualizowana przy aktualizacji bazy danych i aplikacji na innych stacjach - wtedy mamy zonk.

ad2 p2.
No i tu niestety nie będzie błędu zgłaszanego przez DB, bo model bazy danych celowo nie zakłada FK/PK na większości relacji. Podyktowane innymi warunkami. Słownik ten jest tylko formą podpowiedzi. Ok. ale ten punkt zostawmy bo nie chce się skupiać nad sprawami dla mnie nie istotnymi.

Ty chyba oczekujesz cudu. Skoro nie chcesz wykorzystać ORMa, który zrobiłby całą magię za Ciebie, to musisz niestety zaimplementować tę magię sam. Założę się, że to pierwsze byłoby łatwiejsze i tańsze niż to drugie.

O jakiej magii i CRE'mie mówisz :) ? Przypominam że CRM kosztuje nie tylko jako produkt ale i wdrożenie oraz serwis + aktualizacja. Inną kwestią jest to że nie zawsze da się dostosować CRM'a do swoich "nietypowych" wymagań biznesowych.

PS. A jak widziałeś w/w screen'y to jak twoim zdaniem powinna wyglądać obsługa zapisu obiektu kontrahent do bazy (nowego oraz edytowanego)?

0
karlosss napisał(a)

Ad 2p1 - Definitywnie nie zgodzę się z Tobą jakoby podwójna walidacja (w dwóch warstwach) była złem. Wręcz przeciwnie. Walidacja w aplikacji to odciążenie (sito) serwera BD. Nie ma sensu zawracać głowy silnikowi bazy danych

Właśnie - nie ma sensu zawracać głowy silnikowi bazy danych i spowalniać go walidacją biznesową.
Zmiany reguł biznesowych musisz wprowadzać w dwóch miejscach - logice aplikacji i procedurze składowanej. To jest komplikacja i znaczne wydłużenie czasu tworzenia aplikacji. I po co?

No i jest to sprawdzane podczas uruchamiania aplikacji. Nie ma sensu sprawdzać tego w każdej procedurze bo to dopiero negatywnie wpłynęłoby na wydajność systemu.

Równie dobrze aplikacja może sobie cyklicznie sprawdzać czy nie ma dostępnej nowszej wersji. Albo porównywać np. numer schematu zapisany w swojej konfiguracji i porównywać z numerem zapisanym w tabeli konfiguracyjnej w bazie. W jednym czy drugim przypadku następowałby wymuszony restart aplikacji i instalacja nowej wersji.

No i tu niestety nie będzie błędu zgłaszanego przez DB, bo model bazy danych celowo nie zakłada FK/PK na większości relacji.

Cholerny masochizm. Po co używasz w takim razie bazy danych, skoro nie wykorzystujesz jej podstawowych mechanizmów? Nie lepiej trzymać dane w plikach?
Zapewne nie masz tam też indeksów, więc to jest dopiero rozłożenie wydajności.
Już nie wspominając o tym, że Twoja "przemyślana" procedura sprawdza, czy kod nie występuje w bazie (jakby nie można było ustawić unique) albo trimuje dane (jakby nie mogła dostać ich już przetworzonych). Gdzie Ty w ogóle widzisz tu wydajność?

O jakiej magii i CRE'mie mówisz :) ?

Napisałem ORM, a nie CRM. Gdzieś Ty CRM wyczytał? :|

Utrudniasz sobie życie, a potem szukasz obejść dla problemów, które sobie sam stworzyłeś. Nie jestem pewien, czy to dobre podejście.

0

Równie dobrze aplikacja może sobie cyklicznie sprawdzać czy nie ma dostępnej nowszej wersji. Albo porównywać np. numer schematu zapisany w swojej konfiguracji i porównywać z numerem zapisanym w tabeli konfiguracyjnej w bazie. W jednym czy drugim przypadku następowałby wymuszony restart aplikacji i instalacja nowej wersji.

Aplikacja sprawdza każdorazowo przy uruchamianiu czy jest nowa wersja dostępna oraz czy obecnie uruchamiana aplikacja jest zgodna ze schematem bazy danych. Ten temat uważam za zamknięty. Działa to u mnie sprawnie, a inne kwestie związane z tym wątkiem zostały odgórnie narzucone i nie ma co tutaj zmieniać.

Cholerny masochizm. Po co używasz w takim razie bazy danych, skoro nie wykorzystujesz jej podstawowych mechanizmów? Nie lepiej trzymać dane w plikach?
Zapewne nie masz tam też indeksów, więc to jest dopiero rozłożenie wydajności.
Już nie wspominając o tym, że Twoja "przemyślana" procedura sprawdza, czy kod nie występuje w bazie (jakby nie można było ustawić unique) albo trimuje dane (jakby nie mogła dostać ich już przetworzonych). Gdzie Ty w ogóle widzisz tu wydajność?

Nie lubię gdy ktoś nie ma pojęcia o całokształcie, a krytykuje dane rozwiązania. Zamiast zapytać się dlaczego tak a nie inaczej, postanowiłeś krytycznie wypowiedzieć się na ten temat nie znając reali i pełnego obrazu projektu. Niepotrzebnie. Tylko dla twojej wiadomości przedstawię Ci cześć koncepcji projektu DB.

Tabele są tak skonstruowane że zawierają w sobie historię danego obiektu. (Wymagania projektu). Tak więc nie muszę tworzyć dodatkowych tabel (2*n tabel) na przechowywanie archiwum. Aplikacja korzysta z widoków (clastered) dzięki temu tabela przechowuje dane aktualne i historyczne a widok tylko dane aktualne.
Są to tabele że tak powiem referencyjne (starsze rekordy wskazują na nowsze) Spójrz na projekt jednej z nich.
idr to identyfikator rekordu a ido to identyfikator obiektu.

 
/* 
	Słownik państw.

Kod tabeli: 
*/
IF EXISTS (SELECT 1 FROM [sys].[tables] WHERE [name]  = N'country')
    DROP TABLE [pap].[country]
GO

CREATE TABLE [pap].[country] (
	[idr] 				[pap].[idr]			 				NOT NULL,
	[ido] 				[pap].[ido]							NOT NULL,
	[name]				nvarchar(100)						NOT NULL,
	[code2]				nvarchar(2)							NOT NULL,
	[code3]				nvarchar(3)							NOT NULL,
	[nationality]		nvarchar(100)						NULL,
	[ue]				bit									NOT NULL, -- Kraj UE (0-NIE, 1-TAK)
	[status] 			tinyint 							NOT NULL,
	[system]			bit									NOT NULL,
	[next_idr] 			[pap].[idr]							NULL
) ON [DATA];

/* Dodanie PK */
ALTER TABLE [pap].[country]
    ADD CONSTRAINT [pk_country_idr] PRIMARY KEY NONCLUSTERED ([idr] ASC);

/* Dodanie DEFAULT */
ALTER TABLE [pap].[country]
    ADD CONSTRAINT [def_country_system] DEFAULT (NULL) FOR [system];

/* Dodanie DEFAULT */
ALTER TABLE [pap].[country]
    ADD CONSTRAINT [def_country_next_idr] DEFAULT (NULL) FOR [next_idr];

Widok dla odmiany:

-- ****************************************************************************
-- Widok: act_country
-- ****************************************************************************
--
-- Słownik krajów (aktualne) bez historii
-- Bez rekordów z polem next_idr = idr
 
IF EXISTS (SELECT 1 FROM [sys].[views] WHERE [name] = N'act_country')
    DROP VIEW [pap].[act_country]
GO

SET ARITHABORT ON  
SET CONCAT_NULL_YIELDS_NULL ON
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
SET ANSI_PADDING ON 
GO

CREATE VIEW [pap].[act_country]
WITH SCHEMABINDING, ENCRYPTION 
AS
    SELECT  
        [idr],
        [ido],
        [name],
        [code2],
        [code3],
        [nationality],
        [ue],
        [status],
        [system]
    FROM 
        [pap].[country] [c] 
    WHERE 
        [c].[next_idr] IS NULL -- tylko najnowsze rekordy
GO

-- Index: Unique clustered Primary key
IF EXISTS (SELECT 1 FROM [sys].[indexes] WHERE [name] = N'idx_act_country')
    DROP INDEX [pap].[act_country].[idx_act_country];
GO

CREATE UNIQUE CLUSTERED INDEX [idx_act_country]
    ON [pap].[act_country]([ido] DESC)
GO
 

Co do twojego "ironicznego" komentarza dlaczego nie pliki to napisałem wcześniej że " ....nie zakłada FK/PK na WIĘKSZOŚCI relacji...." Gdy ogarniesz budowę tabel i widoków zrozumiesz dlaczego.

Następnym razem postaraj się być nieco bardziej elastyczny :) i wykaż się chęcią poznania a nie nieuzasadnionej krytyki. :)

Napisałem ORM, a nie CRM. Gdzieś Ty CRM wyczytał? :|

Faktycznie. Późna pora była już i O przeczytałem jako C.

Ponawiam pytanie. Jak Ty rozwiązałbyś zapis danych do bazy w takiej aplikacji jak widziałeś u mnie?

0
karlosss napisał(a)

Co do twojego "ironicznego" komentarza dlaczego nie pliki to napisałem wcześniej że " ....nie zakłada FK/PK na WIĘKSZOŚCI relacji...." Gdy ogarniesz budowę tabel i widoków zrozumiesz dlaczego.

Nie będę ogarniał budowy tabel i widoków w Twojej bazie. Mogę Ci tylko napisać, że do trzymania danych historycznych stosuje się oddzielne tabele - ze względów wydajnościowych oraz łatwości w utrzymaniu i modyfikacji aplikacji.

i wykaż się chęcią poznania a nie nieuzasadnionej krytyki. :)

Krytyka nie jest uzasadniona. Wszystko masz zrobione nie tak jak powinno, masz chyba wszystkie możliwe błędy związane z konstrukcją bazy danych i dostępem aplikacji. Bez urazy, ale to chyba Ty nie wykazałeś się chęcią poznania na wczesnym etapie tworzenia aplikacji.

Ponawiam pytanie. Jak Ty rozwiązałbyś zapis danych do bazy w takiej aplikacji jak widziałeś u mnie?

Wracając do tematu - jeżeli chodzi o zagadnienie zapisu grafu obiektów do bazy za pomocą procedur składowanych, to widzę to tak:

  1. Zapisanie obiektu głównego (procedura sama powinna wiedzieć, czy ma to być insert czy update).
  2. Pobranie jego ID i ustawienie w obiektach podrzędnych.
  3. Zapis obiektów podrzędnych.
  4. Wywoływać rekurencyjnie jeśli obiekty podrzędne też mają swoje obiekty podrzędne.
  5. Zamknąć to w jakimś TransactionScope, żeby w razie czego można wszystko wycofać.
0

OT:
czy widok z indeksem clustered zachowuje się jak tabela?

PS
Czy wyobrażasz sobie 300 tabel + dodatkowe 300 na przechowywanie historii ?
Inną kwestią jest fakt o którym ciągle wspominam - część koncepcji została narzucona i nie mam na to wpływu. Robiłem też testy wydajnościowe przy 25 mln rekordów w tabeli gdzie w widoku korzystającym z tej tabeli jest 5 tys. aktualnych danych. Wyniki są bdb. I co ty na to?

No w końcu dostałem odp. :) Dzięki. Podobnie myślałem tylko jeszcze mam mały problem z implementacją. Ale jakoś będziemy działać.

0
karlosss napisał(a)

czy widok z indeksem clustered zachowuje się jak tabela?

A czy wprowadzenie danych do tabeli na którą nałożony jest klastrowany widok jest równie efektywne co do samej tabeli? ;>

Czy wyobrażasz sobie 300 tabel + dodatkowe 300 na przechowywanie historii ?

Nie muszę sobie niczego wyobrażać, jutro znowu pójdę do pracy i zobaczę taki system. Działający w rzeczywistym świecie i używany przez kilka tysięcy osób.
Pracuję z takimi systemami. W jednych historia aktualizowana jest przez aplikację, w innych przez triggery. Ale wszędzie jest PK i FK, jak w normalnych bazach danych.

Jak identyfikujesz aktualny rekord z tabeli aktualno-historycznej? Wybierając ten o najświeższej dacie utworzenia? Czy jest to równie efektywne jak wybranie jedynie po PK?

Wyniki są bdb. I co ty na to?

Co ja na co? Co to są "wyniki bdb."? Obiektywne stwierdzenie poproszę, jeśli mam się do niego odnieść.
I zakładam, że testy porównałeś z bazą danych o takiej samej strukturze, ale z PK, FK, Unique i bez procedur wykonujących nadmiarowe operacje.

No w końcu dostałem odp. :) Dzięki. Podobnie myślałem tylko jeszcze mam mały problem z implementacją. Ale jakoś będziemy działać.

Szczerze życzę powodzenia.

I nie krytykuję Ciebie tylko koncepcji, która jest porąbana i utrudnia Twoje życie, nie moje.

0

Tak się składa że też pracuję z takimi systemami gdzie jest ponad kilka tys. użytkowników. I prezentowany przez zemnie model bardzo dobrze sprawdza się w środowisku produkcyjnym. Nie ukrywam że część pomysłów zapożyczyłem. Co do ilości tabel chodzi też o koszt czasowy, gdy aplikację+bazę tworzy jedna osoba a nie zespół programistów.
Jeśli uważnie czytałbyś to zauważyłbyś że w moich strukturach występują PK, FK tam gdzie to jest niezbędne.

Podziękuję Ci już za Twoje refleksje. Chętnie poczytałbym opinie innych użytkowników, bo z zasady nigdy nie kieruje się głosem jednego "autorytetu" :)

Miłego dnia w pracy.

0
karlosss napisał(a)

Podziękuję Ci już za Twoje refleksje. Chętnie poczytałbym opinie innych użytkowników, bo z zasady nigdy nie kieruje się głosem jednego "autorytetu" :)

myślę, że innego zdania tu nie uświadczysz gdyż większość zgadza się z somekind'em a przynajmniej ja. Z drugiej strony mnie osobiście znudziły się dyskusje, gdzie pytacz pyta tylko po to aby mu przytaknąć i pogłaskać po główce mówiąc, jaki to on jest świetny a jego rozwiązania genialne. Mamy tu kilku takich (np. J...s :p) i o ile na początku było to śmieszne to teraz po prostu jest nudne

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