Changelog

0

Witam serdecznie,

Chciałbym zrobić coś takiego jak changelog, mam tabele firmy która ma ok 30 kolumn i tabele changeLog, przy aktualizacji rekordu z tabeli firmy chciałbym utworzyć rekord w tabeli changeLog z następującymi danymi: która kolumna została zmieniona, stara wartość, nowa wartość i kilka innych, ale z tym wiem jak sobie poradzić, wszelkie sugestie mile widziane.

0

mozesz to zrobic na poziomie bazy

  1. albo wszystkie operacje wykonujesz przez procedury (czyli insert, update, delete), ktore maja zaszyte mechanizmy tworzenia takze rekordu w changeLogu
  2. trigger na tabeli firmy
    albo mozesz to zrobic na pozionie warstwy dostepu do danych, jezeli taka masz
0

Jak rozumiem, chcesz to zrobic niezaleznie od dzialania samej aplikacji (korzystajac z samego jezyka SQL). Do tego musisz uzyc mechanizmu zwanego triggerami. Czyli tworzysz nowy trigger, ktory bedzie wywolywany po kazdej aktualizacji rekordu w tabeli firmy. Tabela changelog moze wygladac tak:

changelog_firm # ID firmy (klucz obcy)
changelog_old # stara wartosc
changelog_new # nowa wartosc
changelog_time # timestamp - czas dodania rekordu

Teraz sam trigger:

DELIMITER //
CREATE TRIGGER 'foo' AFTER UPDATE ON 'firmy' FOR EACH ROW
BEGIN

END//

Teraz w tym triggerze najprosciej byloby zrobic warunek:

IF OLD.nazwa_firmy != NEW.nazwa_firmy THEN
   INSERT INTO changelog (changelog_firm, changelog_old, changelog_new, changelog_timestamp) VALUES(NEW.firm_id, OLD.nazwa_firmy, NEW.nazwa_firmy, UNIX_TIMESTAMP());
END IF;

Ale dla 30 kolumn robienie takich warunkow jest bez sensowne... :/

0

Dzięki za szybką odpowiedź,

Generalnie Massther rozważałem 1 opcje i w zasadzie, część z tego zrobiłem (w momencie kiedy myślałem, że chodzi o logowanie 2-3 kolumn) ale robienie tego dla 30 jest zbyt czaso- i pracochłonne (chyba, że sposób który ja wybrałem jest po prostu zły, a mianowicie mam kolumnę status, przy aktualizacji rekordu sprawdzam starą wartość jeśli nowa jest inna to tworzę w tabeli changelog rekord z pożądanymi danymi, ale nie wyobrażam sobie tego robić w ten sposób dla 30 kolumn, dlatego poszukuję innego rozwiązania), mógłbyś trochę bardziej przybliżyć opcję z dostępem do warstwy danych ?

@Adam
Nie koniecznie, może to być zrealizowane także w aplikacji, szukam najlepszego rozwiązania. Jeszcze raz wielkie dzięki za pomoc !

0

warstwa dostepu do danych to twoje api, przez ktore wszystkie elementy aplikacji pobieraja dane z bazy
mega uproszczenie
masz okienko pokazujace liste klientow (imie, nazwisko, etc.)
to nie tworzysz w nim polaczenia do bazy i pobrania rekordow, tylko masz do tego osobna klase (czy tylko metode w jakiejs klasie), np. Clients.GetAll(), ktora tworzy polaczenie, pobiera rekordy i zwraca je w postaci listy obiektow (w tym przypadku klient)
analogicznie operacje modyfikujace dane, np. Clients.CreateClient("Jan", "Kowalski", ...) lub Clients.ModifyClient(clientObject)
tylko ze w nich ustalasz jakie dane zostaly zmienione i w dokonujesz wstawienia odpowiednich wpisow do changelog

jak mozna zrealizowac taka warstwe zalezy jakiego jezyka programowania uzywasz i jakis innych mechanizmow (np. OR mappery)

napisz w jakiej technologii masz aplikacje

0

Api jest w .net 4.0, c#.

No ale generalnie w tej klasie (czy tam metodzie) będę to musiał zrobić tak jak zrobiłem dotychczas ? Tzn sprawdzić obecna wartość jeśli nowa jest inna to wstawić wpis do ChangeLog ? W sumie to bardziej chodzi mi o to, które rozwiązanie będzie najlepsze czy zrobić to przez modyfikacje zapytań sql czy tak jak mówisz w warstwie dostępu do danych?

0

jesli aplikacje masz w c# to jesli uzyjesz np. linq to sql lub entity framework, mozesz latwo dodac funkcjonalnosc ktora chcesz, ktora zadziala dla kazdej tabeli
jesli uzywasz bezposrednio ado .net, tez nie ma problemu, bo mozesz zalozyc implementacja jakis interface, ktore pomoga ci osiagnac cel lub w ostatecznosci uzyc reflection do porownywania wartosci dowolnych obiektow (ktore odwzorowywuja rekordy w aplikacji)

  1. trigger - zaloguje kazda zmiane wprowadzona nawet nie z poziomu aplikacji
  2. procedury skladowane - zaloguja tylko zmiany wprowadzanie przy uzyciu procedur, nie zaloguja zmian dokonanych przez bezposrednie wykonanie jakiejs query modyfikujacej dane
  3. w aplikacji - bedzie logowalo jedynie zmiany dancyh dokonane z poziomu aplikacji, umozliwia latwiejsze porownywanie zmian zlozonych struktur danych, ale obarczone jest wieksza zlozonoscia obliczeniowa i/lub pamieciowa, mozesz stworzyc bardziej uniwersalny mechanizm
    dla 1 i 2 tez mozna stworzyc uniwersalne mechanizmy, ale wymaga to budowania dynamicznych zapytan i odpytywania tabel systemowych o strukture mowyfikowanej tabeli, wiec wiekszej wiedzy o sql i systemie bazodanowym (uzywasz MS SQL Server 2005 czy 2008 czy mySQL, czy czegos innego?)

czyli kazde z rozwiazan ma wady i zalety, od ciebie zalezy co jest dla ciebie istotne
jak chcesz zeby ktores rozwiazanie dokladniej opisac i wskazac wiecej wad/zalet pisz

0

Nie wiem czy to tylko u mnie, ale mam problemy z przeglądaniem forum. W każdym razie jeśli byłbyś uprzejmy opisać bardziej 3 opcje, używam mssql2008 :).

Pozdrawiam

0
  1. Linq to SQL
    jak wyklikasz sobie diagram linq to sql bedziesz mial klase, ktora dziedziczy po System.Data.Linq.DataContext (zalozmy MyDataContext)
    najlepiej zrobic kolejna klase dziedziczaca po twojej (np. My2DataContext : MyDataContext) i przeciazyc w niej metode SubmitChanges
private HashSet<Type> _logFilter = new HashSet<Type>(new[] { 
            typeof(SomeType1), 
            typeof(SomeType2),
            ...
        });

public override void SubmitChanges(System.Data.Linq.ConflictMode failureMode)
        {
            ChangeSet cs = this.GetChangeSet();

#if DEBUG
            int insCount = 0, updCount = 0, delCount = 0;
#endif

            List<DbAction> actionsToLog = new List<DbAction>(cs.Inserts.Count + cs.Updates.Count + cs.Deletes.Count);
            for (int i = 0; i < cs.Updates.Count; i++)
            {
                Type entityType = cs.Updates[i].GetType();
                if (!_logFilter.Contains(entityType))
                {
#if DEBUG
                    ++updCount;
#endif
                    object originalEntity = this.GetTable(entityType).GetOriginalEntityState(cs.Updates[i]);
                    ModifiedMemberInfo[] changes = this.GetTable(entityType).GetModifiedMembers(cs.Updates[i]);

                    DbAction act = new DbAction();
                    act.User = ...
                    act.Date = DateTime.Now;
                    act.ActionType = DbAction.ActionTypeEnum.Upd;
                    act.EntityTypeName = entityType.Name;
                    act.Changes = new List<ModifiedMemberInfo>(changes);
                    act.OldData = originalEntity;
                    act.NewData = cs.Updates[i];
                    actionsToLog.Add(act);
                }
            }
            for (int i = 0; i < cs.Inserts.Count; i++)
            {
                Type entityType = cs.Inserts[i].GetType();
                if (!_logFilter.Contains(entityType))
                {
#if DEBUG
                    ++insCount;
#endif
                    DbAction act = new DbAction();
                    act.User = ...
                    act.Date = DateTime.Now;
                    act.ActionType = DbAction.ActionTypeEnum.Ins;
                    act.EntityTypeName = entityType.Name;
                    act.OldData = null;
                    act.NewData = cs.Inserts[i];
                    actionsToLog.Add(act);
                }
            }
            for (int i = 0; i < cs.Deletes.Count; i++)
            {
                Type entityType = cs.Deletes[i].GetType();
                if (!_logFilter.Contains(entityType))
                {
#if DEBUG
                    ++delCount;
#endif
                    DbAction act = new DbAction();
                    act.User = ...
                    act.Date = DateTime.Now;
                    act.ActionType = DbAction.ActionTypeEnum.Del;
                    act.EntityTypeName = entityType.Name;
                    act.OldData = cs.Deletes[i];
                    act.NewData = null;
                    actionsToLog.Add(act);
                }
            }
#if DEBUG
            if (insCount + updCount + delCount > 0)
                Console.WriteLine("ChangeSet: Ins: {0} | Upd: {1} | Del: {2} ", insCount, updCount, delCount);
#endif

            // bazowe wywolanie, ktore wykona akcje na bazie
            base.SubmitChanges(failureMode);
            // bazowy SubmitChanges wewnetrznie wykonuje wszystko w tranzakcji
            // wiec jesli zakonczy sie wyjatkiem, to takze akcje nie powinny sie zalogowac
            actionsToLog.ForEach(a => DbActionsLogger.LogAction(a));
            actionsToLog.Clear();
            actionsToLog = null;
        }

        public static void LogAction(DbAction act)
        {
            Thread.BeginCriticalRegion();
            lock (((ICollection)_q).SyncRoot) // synchronizacja dostepu do kolejki
            {
                _q.Enqueue(act);
            }
            _sem.Release();
            Thread.EndCriticalRegion();
        }
private static void LogJob()
        {
            while (_sem.WaitOne())
            {
                Thread.BeginCriticalRegion();
                try
                {
                    DbAction actToLog = null;
                    lock (((ICollection)_q).SyncRoot) // synchronizacja dostepu do kolejki
                    {
                        if (_q.Count <= 0)
                            continue;
                        actToLog = _q.Dequeue();
                    }
                    if (actToLog != null)
                    {
                        // zmiany sa tylko dla akcji Update
                        if (actToLog.Changes != null && actToLog.Changes.Count > 0)
                            actToLog.Changes = actToLog.Changes.Where(mm => mm.Member.Name != "UpdatedBy" && mm.Member.Name != "UpdatedDate").ToList();

                        // loguj akcje Update tylko jesli faktycznie sa zmiany danych
                        if (!(actToLog.ActionType == DbAction.ActionTypeEnum.Upd && (actToLog.Changes == null || actToLog.Changes.Count == 0)))
                        {
                            LogDbAction entry = new LogDbAction()
                            {
                                User = actToLog.User,
                                ActionType = Enum.GetName(typeof(DbAction.ActionTypeEnum), actToLog.ActionType),
                                ObjectName = actToLog.EntityTypeName,
                                Date = actToLog.Date,
                                Changes = actToLog.GetChanges(),
                                OldData = DbAction.GetSerializedEntity(actToLog.OldData),
                                NewData = DbAction.GetSerializedEntity(actToLog.NewData)
                            };
                            //if (entry.ActionType.Length > 3)
                            //   entry.ActionType = entry.ActionType.Substring(0, 3);
                            _db.LogDbActions.InsertOnSubmit(entry);
                            _db.SubmitChanges();
                        }
                    }
                }
                catch (Exception ex)
                {
                    ...
                }
                Thread.EndCriticalRegion();
            }
        }
public class DbAction
    {
        public enum ActionTypeEnum
        {
            Ins, Upd, Del
        }
        public string User { get; set; }
        public DateTime Date { get; set; }
        public ActionTypeEnum ActionType { get; set; }
        public List<ModifiedMemberInfo> Changes { get; set; }
        public object OldData { get; set; }
        public object NewData { get; set; }
        public string EntityTypeName { get; set; }

        public DbAction()
        {
            Changes = new List<ModifiedMemberInfo>();
        }

        /// <summary>Serialize changes collection.</summary>
        /// <returns>Serialized changes collection or null if collection is null or empty.</returns>
        public XElement GetChanges()
        {
            if (Changes == null || Changes.Count == 0)
                return null;

            XElement el = null;
            XmlWriter xw = null;
            try
            {
                el = new XElement("Changes");
                xw = el.CreateWriter();
                Changes.ForEach(mm =>
                {
                    xw.WriteStartElement("Field");
                    xw.WriteElementString("Name", mm.Member != null ? mm.Member.Name : "NULL");
                    xw.WriteElementString("OldValue", mm.OriginalValue != null ? mm.OriginalValue.ToString() : string.Empty);
                    xw.WriteElementString("NewValue", mm.CurrentValue != null ? mm.CurrentValue.ToString() : string.Empty);
                    xw.WriteEndElement();
                });
            }
            catch (Exception ex)
            {
                ...
            }
            finally
            {
                if (xw != null) xw.Close();
            }

            return el;
        }
        /// <summary>Serialize linq to sql entity. Serialize only properties witch attribute ColumnAttribute.</summary>
        /// <param name="o">Object to serialize</param>
        /// <returns>Serialized object or null if object is null or error occured during serialization.</returns>
        public static XElement GetSerializedEntity(object o)
        {
            if (o == null)
                return null;

            Type entityType = o.GetType();
            XElement el = null;
            XmlWriter xw = null;
            try
            {
                el = new XElement(entityType.Name);
                xw = el.CreateWriter();

                PropertyInfo[] props = entityType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
                for (int i = 0; i < props.Length; i++)
                {
                    object[] attrs = props[i].GetCustomAttributes(typeof(System.Data.Linq.Mapping.ColumnAttribute), false);
                    if (attrs != null && attrs.Length > 0 && props[i].CanRead)
                    {
                        object value = props[i].GetValue(o, null);
                        xw.WriteElementString(props[i].Name, value != null ? value.ToString() : string.Empty);
                    }
                }
            }
            catch (Exception ex)
            {
                ...
            }
            finally
            {
                if (xw != null) xw.Close();
            }

            return el;
        }
    }
CREATE TABLE [dbo].[LogDbActions](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[User] [nvarchar](50) NOT NULL,
	[Date] [datetime] NOT NULL,
	[ActionType] [char](3) NOT NULL,
	[ObjectName] [nvarchar](100) NOT NULL,
	[Changes] [xml] NULL,
	[OldData] [xml] NULL,
	[NewData] [xml] NULL,
 CONSTRAINT [PK_LogDbActions] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

coz jak widzisz nie jest to najprostsze, ale swietnie dziala, umozliwia odtworzenie pelnej historii rekordu
nie moglem calosci ci podac, ale to co napisalem powinno wystarczyc
LogJob chodzi w osobnym watku i zapisuje wszelkie zmiany do bazy (nie za czesto, zeby nie obciazac jej specjalnie)
w entity framework zasada bedzie podobna

jesli swtorzysz jakas autorska DAL, to generalna zasada jest ta sama, musisz miec stan oryginalny i zmieniony i wykryc gdzie nastapily modyfikacje, a nastepnie je zapisac

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