Dodanie parametru table-valued spowalnia wykonanie zapytania

sql, optymalizacja, ses, eventsourcing

Próba optymalizacji

Podczas implementacji Ses.MsSql chciałem jak najbardziej zoptymalizować zapis danych do bazy. Jedną ze znanych metod jest zmniejszenie ilości odwołań do serwera bazy danych.

Kiedy pojawia się więcej niż jeden rekord do zapisania standardowo zapisujemy w pętli jeden po drugim. Niestety każdy zapis to wywołanie zapytania typu INSERT, czyli strzał do bazy. A gdyby tak wysłać do serwera wszystkie rekordy naraz?

MS SQL Server od wersji 2008 posiada taką możliwość. Możemy zdefiniować własny typ, na przykład:

CREATE TYPE dbo.NewEvents AS TABLE (
    [Version] INT NOT NULL,
    [ContractName] NVARCHAR(225) NOT NULL,
    [Payload] VARBINARY(MAX) NOT NULL
)

Teraz kawałek SQLa, w którym skorzystamy z naszego nowego typu:

--DECLARE @Events NewEvents

INSERT INTO [Streams]([StreamId],[CommitId],[Version],[ContractName],[Payload],[CreatedAtUtc])
SELECT
    @StreamId,
    @CommitId,
    [Version],
    [ContractName],
    [Payload],
    @CreatedAtUtc
FROM
    @Events
ORDER BY
    [Version];

Taka konstrukcja pozwala na wrzucenie do tabeli Streams wszystkich rekordów przesłanych w strukturze NewEvents. Pokazany powyżej przykład zapytania to tylko wycinek procedury, która odpowiedzialna jest za dodawanie nowych zdarzeń do bazy projektu Ses.MsSql.

No dobra, mamy załatwioną część po stronie sqla. Teraz kod c#:

await cmd
    .AddInputParam(SqlQueries.InsertEvents.ParamStreamId, DbType.Guid, streamId)
    .AddInputParam(SqlQueries.InsertEvents.ParamCommitId, DbType.Guid, commitId)
    .AddInputParam(SqlQueries.InsertEvents.ParamCreatedAtUtc, DbType.DateTime, DateTime.UtcNow)
    .AddInputParam(SqlQueries.InsertEvents.ParamMetadataPayload, DbType.Binary, metadata, true)
    .AddInputParam(SqlQueries.InsertEvents.ParamIsLockable, DbType.Boolean, isLockable)
    .AddInputParam(SqlQueries.InsertEvents.ParamEvents, records)
    .ExecuteNonQueryAsync(cancellationToken)
    .ConfigureAwait(false);

gdzie records to poprostu:

IEnumerable<SqlDataRecord>

Typ SqlDataRecord został dodany do framework.net już w wersji 2.0.

Pokazany powyżej przykład daje (powinien dać) nam taką przewagę nad pojedynczym insertem, że przesłanie jednego czy wielu rekordów nie powinno zwiększać czasu wykonania (przynajmniej w pewnej skali).

I faktycznie tak jest. Czy przesyłam 1, 10 czy 100 rekordów to nadal czasy wykonania są podobne - zachowuje pewną liniowość.

Fiasko

Niestety kiedy wykonałem pomiary dla procedury bez parametru typu table-valued to wyszło mi, że cała konstrukcja wykonuje się ponad 9 razy szybciej. Problem opisywałem już jakiś czas temu na SO. Do tej pory nie znalazłem rozwiązania.

Wnioski

Taka optymalizacja przynajmniej w tym przypadku traci jakikolwiek sens.

Co więcej, większość biznesowych operacji generuje zazwyczaj jedno (dwa) zdarzenia. Dla takiej ilości wystarczająco wydajnym mechanizmem jest zapis zdarzeń rekord po rekordzie, a na dodatek w przypadku wystąpienia jakiegokolwiek błędu (błąd wersji agregatu - concurrency) od razu dostajemy informację, na którym zdarzeniu się to stało i możemy bezpośrednio podjąć odpowiednie kroki. O czym w kolejnych wpisach.

SES - Podstawowe interfejsy

ses, eventstore, eventsourcing

Dzisiaj zrobiłem kilka zmian, choć nie wiele, bo miałem na głowie inną pilną robotę. Wieczorkiem usiadłem i w sumie nie wszystko co wcześniej napisałem mi się podoba. Jutro zatem duże zmiany.

Mimo to jest kilka rzeczy, które w miarę się krystalizują i prawdopodobnie w tej postaci zostaną zachowane.

IEventStore

Podstawowy interface całej biblioteki, czyli IEventStore.

public interface IEventStore
{
    Task<IReadOnlyEventStream> Load(
        Guid streamId,
        bool pessimisticLock,
        CancellationToken cancellationToken = default(CancellationToken));

    Task SaveChanges(
        Guid streamId,
        int expectedVersion,
        IEventStream stream,
        CancellationToken cancellationToken = default(CancellationToken));
}

Definiuje tylko dwie metody:

  • Load - wczytuje dane z bazy danych
  • SaveChanges - zapisuje nowe zdarzenia do bazy danych

Oczywiście pod nimi kryje się troszkę logiki takiej jak serializacja/deserializacja, konwersja do nowszej wersji, rozwiązywanie konfliktów w przypadku wykrycia kolizji wersji, itd.

IEvent (i IMemento)

Zdarzenie marker, ale ułatwia kilka rzeczy. Po pierwsze w łatwy sposób mogę posługiwać się snapshotem, gdzie jeśli istnieje to zawsze będzie pierwszym elementem w liście odczytanych zdarzeń, bo IMemento dziedziczy po IEvent.

Dzięki temu mogę zrobić tak:

var events = await _settings.Persistor.Load(streamId, pessimisticLock);
var snapshot = events[0] as IMemento;
var currentVersion = snapshot?.Version + events.Count ?? events.Count;
return new ReadOnlyEventStream(events, currentVersion);

Słowem wyjaśnienia. Ładujemy zdarzenia. Sprawdzamy, czy pierwszy element to IMemento. Jeśli tak to znaczy, że mamy załadowany snapshot, a zdarzenia są tylko te, które mają wersję od niego starszą. Jeśli nie jest to IMemento to znaczy, że mamy załadowane wszystkie zdarzenia dla danego streamId.

Takie podejście przekłada się także na to jak będzie wyglądała implementacja IAggregate, ale o tym innym razem.

IReadOnlyEventStream

Definuje obiekt, w którym dostaniemy w całości załadowane dane dla danego streamId.

public interface IReadOnlyEventStream
{
    IReadOnlyList<IEvent> Events { get; } // Lista zdarzeń
    int CurrentVersion { get; } // Bieżąca wersja w bazie danych
}

IEventStream

W odróżnieniu od poprzedniego interfejsu IEventStream definiuje nam obiekt, który wysyłamy do eventstore nowe zdarzenia, aby zapisał je do bazy danych.

public interface IEventStream
{
    Guid CommitId { get; } // Identyfikator zmiany
    IEvent[] Events { get; } // Lista nowych zdarzeń
    IDictionary<string, object> Metadata { get; set; } // Metadane, związane z zapisywanymi zdarzeniami
}

PesimissticLock

Pewnie intryguje Was ten parametr. I słusznie. W sumie to się zastanawiam czy nie powinienem zmienić, aby miał domyślną wartość na false. A skąd w ogóle taki pomysł, aby coś takiego umieszczać w EventStore? O tym w kolejnym wpisie. :)

Simple Event Store

ses, eventstore, eventsourcing

Właśnie umieściłem na githubie zarys projektu SimpleEventStore (w skrócie SES). W założeniach ma to być biblioteczka (library - not framework) wspomagająca tworzenie aplikacji opartych o eventsouring (ES), gdzie dane są przechowywane w relacyjnej bazie danych. Na początek w podstawowej implementacji MS SQL Server.

Główne założenia do projektu:

  • całe API asynchroniczne (async/await)
  • bez dodatkowych zależności
  • możliwość użycia dowolnego silnika relacyjnej bazy danych
  • wsparcie dla optymistycznego (i pesymistycznego dla szczególnych przypadków) blokowania
  • możliwość użycia dowolnej biblioteki do logowania dzięki interfejsowi ILogger
  • możliwość użycia dowolnej biblioteki do serializacji/deserializacji danych dzięki interfejsowi ISerializer
  • automatyczna konwersja zdarzeń do nowszej wersji
  • wbudowany i łatwo podmienialny mechanizm subskrypcji
  • idempotentny subskrybent z automatu

Dodatkową zaletą ma być odczyt zdarzeń przy zachowaniu porządku zapisu. Przedstawię to na następującym przykładzie. Załóżmy, że klient rejestruje się w naszej aplikacji. Z punktu widzenia logiki biznesowej ważne jest, aby wiedzieć, że klient się zarejestrował (jedno zdarzenie) oraz dodatkową informacją jest to, że zaakceptował możliwość odbierania informacji handlowych w postaci newslettera (drugie zdarzenie).

Nasz agregat (User) wyemituje zatem dwa zdarzenia:

  • UserRegistered
  • UserNewsletterAccepted

SimpleEventStore w podstawowej implementacji (czyli dla MS SQL Server) będzie gwarantował, że zapisane zdarzenia będą mogły być przetwarzane przez subskrybentów zawsze w tej samej kolejności. Nie będzie możliwa sytuacja, że system będzie chciał oznaczać możliwość wysyłania newslettera do niezarejestrowanego jeszcze użytkownika.

Przetwarzanie zdarzeń bez zachowania kolejności budzi wiele problemów. Ten temat powraca jak bumerang na łamach grupy DDD/CQRS/ES.

Wszystko to co powyżej zostało określone w założeniach da się wykonać dzięki temu, że relacyjne bazy danych wspierają transakcje. To bardzo ułatwia. :)

Poniżej przykładowy (oczekiwany) kod obrazujący api SES:

var options = new TransactionOptions {IsolationLevel = IsolationLevel.ReadCommitted};
using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
{
    var id = SequentalGuid.NewGuid();

    var aggregate = new ShoppingCart();
    aggregate.AddItem(SequentalGuid.NewGuid(), name: "Product 1", quantity: 3);


    var stream = new EventStream(id)

    // Appending events
    stream.Append(aggregate.TakeUncommittedEvents());

    // Adding metadata item (key, value)
    stream.Advanced.AddMetadata("RequestIP", "0.0.0.0");
    stream.Advanced.AddMetadata("User", "John Doe");

    await store.SaveChanges(stream);

    await scope.Complete();
}

A teraz z użyciem Repository, który skrzętnie ukrywa niepotrzebne szczegóły z punktu widzenia obsługi logiki samej aplikacji:

using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
{
    using (var repo = new SourcedRepo<ShoppingCart>(store))
    {
        var aggregate = await repo.Load(id);

        aggregate.AddItem(Guid.NewGuid(), "Product 1", 3);

        await repo.SaveChanges(aggregate);
    }
    scope.Complete();
}

Na potrzeby jednego z projektów popełniłem już podobne rozwiązanie. Niestety wcześniej zbyt mocno wzorowałem się na NEventStore. Nie wpłynęło to dobrze na prawidłową implementację wcześniejszego rozwiązania. Zresztą jeden z commiterów NES także sam zauważył, że NES poszedł w złym kierunku.

Na pierwszy rzut oka można sobie pomyśleć 'to nie będzie działać wydajnie'. Nic bardziej mylnego. W poprzednim rozwiązaniu na moim i5 (dysk SSD) spokojnie dało się wyciągnąć ponad 6000 msg/sec. Jak na mój gust całkiem nieźle.

Przesiadka z Wordpressa na Pretzel

blog

Przesiadka z Wordpressa na Pretzel

Dzisiaj postanowiłem coś zrobić ze swoim blogiem. Podoba mi się prostota statycznych generatorów. Pełna kontrola nad wypluwanym kodem html. Do tego łatwość modyfikacji oraz szybkość działania. Wszystko to bez żadnej bazy danych. Pisanie nowego posta to po prostu dodanie nowego pliku.

Nie ma oczywiście tu żadnego edytora. Do edycji Twoich postów może być każdy program, który potrafi edytować pliki tekstowe. Ten tekst piszę w Visual Studio Code, ale równie dobrze mogę użyć Worda wraz ze sprawdzaniem składni. :)

Najlepsze jest to, że większość statycznych generatorów obsługuje Markdown. Prostota tego rozwiązania powala i daje niesamowitą elastyczność w pisaniu. Używając Wordpressa musiałem pokombinować co i jak, aby sklecić cały tekst wraz z obrazkami, wstawkami kodu, itd. Tutaj wklejam czysty kod z notatnika i niemalże po kilku małych poprawkach mam sformatowany post.

Pretzel

Sam Pretzel to statyczny generator stron html, który działa na takiej samej zasadzie jak Jekyll, ale jest napisany w c#. Projekt jest open-source i można go znaleźć na githubie.

Składnia taka sama jak w Jekyll, do tego Markdown i w sumie nic więcej do szczęścia nie potrzeba.

Publikacja nowego posta wymaga uruchomienia generatora z linii komend posługując się poniższą (dowcipną) składnią:

    pretzel bake blog.softio.pl

Po wygenerowaniu w odpowiednim katalogu otrzymujemy pliki wynikowe, które wystarczy, że wgramy (zwykłe copy) na nasz serwer.

Jeśli ktoś zastanawia się nad zmianą platformy to szczerze polecam. Na prosty blog dla deva w zupełności wystarczy.

Czy da się żyć bez Reshapera?

produktywność

Czy da się żyć bez Reshapera?

Resharper

Resharper to narzędzie do wspomagania pracy (życia) programisty. Przez jednych wychwalany pod niebiosa. Przez innych - mieszany z błotem. Aby móc coś powiedzieć o tym narzędziu trzeba go trochę poużywać.

Był taki czas, że dawałem sobie radę bez niego. Długo odwlekałem decyzję z zakupem. Między innymi dlatego, że w pobliżu nie znalazł się nikt kto by chciał mi taki zakup zasponsorować (mówię tu o moich poprzednich pracodawcach). Od dłuższego czasu sam jestem sobie pracodawcą, ale wtedy w grę wchodzą względy finansowe. Resharper dla przysłowiowego Kowalskiego tani nie jest. Mimo to, w końcu zainwestowałem. Na początku troszkę trwało zanim opanowałem podstawowe ruchy, czytaj skróty klawiaturowe, choć de-facto sprowadza się tak naprawdę do jednego, mitycznego Alt+Enter. Tak jest, z Resharperem aplikacje pisze się już prosto, wystarczy kilkanaście razy nastukać Alt+Enter i kodzik napisany. ;) Taki żarcik, a na serio nie wyobrażam (nie wyobrażałem) sobie wykonywania większego refaktoringu bez niego.

Resharper jest narzędziem potężnym i z wersji na wersję coraz bardziej rozbudowanym. Niestety za tym idzie jak cień jego ociężałość. Świeże Visual Studio 2015 śmiga aż miło. Zainstalowanie Resharpera spowalnia czas startu studia, czas załadowania projektu, a ostatnio zacząłem bardzo mocno odczuwać także spowolnienie pracy całego środowiska. W szczególności bardzo denerwujące jest to kiedy studio się zacina podczas wykonywania standardowych operacji (przykładowo zmiana nazwy klasy). Żeby nie było, komp świeżo po reinstalacji, takie tam i7 + SSD + 8GB RAM.

Z tego powodu zacząłem się zastanawiać jak by wyglądała moja codzienność bez Resharpera. Niestety natura człowieka jest taka, że jak się przyzwyczai do pewnych rzeczy to później trudno jest się odzwyczaić. Ja na tym polu niczym szczególnym się nie wyróżniam, więc zacząłem szukać tych małych ulepszaczy, z których korzystam na co dzień, ale bez całej reszty bagażu jaką wnosi Resharper. I tak oto stworzyłem poniższą listę "zastępników".

Move Class To File

Bardzo prosty i mały dodatek, dzięki któremu bardzo łatwo jest przenieść definicję klasy do nowego pliku. Na dodatek taki smaczek - rozszerzenie to jest napisane przez Filipa W. Zastępuje funkcję Resharpera, która pozwala na podzielenie jednego pliku z kilkoma definicjami do odpowiadających im oddzielnym plikom. Podpowiedź wywołuje się z podręcznego menu kontekstowego standardowym dla VS skrótem klawiaturowym "Ctrl+.".

Refactoring Essentials

To drugi dodatek, który zastępuje wiele opcji samego Resharpera. Posiada bardzo rozbudowaną listę wspomagaczy. Nie będę się rozpisywał na temat jego możliwości, sprawdźcie sami. Podpowiedź wywołuje się z podręcznego menu kontekstowego standardowym dla VS skrótem klawiaturowym "Ctrl+.".

VS Tricks

Wszyscy, którzy używają Resharpera na pewno wiedzą co to jest Camel Humps. Niestety dodatek nie wspiera wyszukiwania, gdzie wpisuje się duże litery wyrazów składających się na pełną nazwę (na przykład wpisujemy WPE, a Resharper znajdzie WordProcessorExtensions). Za to doskonale działa przeskakiwanie pomiędzy wyrazami będącymi członami dłuższych, wielowyrazowych nazw. Bardzo przydatne podczas edycji kodu. Wymaga własnej konfiguracji skrótów klawiaturowych w ustawieniach VS (standardowo "Ctrl+Left" i "Ctrl+Right").

Productivity Power Tools

Tego dodatku chyba nie muszę opisywać, gdyż dostępny jest od dawna, nawet dla starszych wersji VS. Pomimo zainstalowanego Resharpera zawsze ten dodatek był pod ręką. Lista opcji jest naprawdę spora. Sam dodatek jest stworzony przez "Visual Studio Product Team".

Clr Heap Allocation Analyzer

Tu dodatek odstający od reszty, bo skupia się na problemach związanych z wydajnością pisanego kodu. Odkryłem go dopiero co i szczerze polecam. A dlaczego polecam? Obejrzyjcie video z wystąpienia Bartka Adamczewskiego na warszawskiej grupie .net.

NUnit Test Adapter

Uruchamianie testów to jest coś co mnie najbardziej boli. Wbudowane narzędzie w Visual Studio jest naprawdę skromne. Na szczęście nikt teraz nie tworzy oprogramowania bez CI, więc jest to do przeżycia. Lokalnie odpalam tylko i wyłącznie szybkie testy jednostkowe. Odpowiednie nazewnictwo daje w miarę dobry pogląd na tej prostej liście jaką daje nam studio.

Warto tu jeszcze wspomnieć o kilku usprawnieniach w samym Visual Studio, które także w ostatniej wersji zyskało kilka fajnych opcji wspomagających kodowanie. To także dzięki nim mogę kliknąć "Suspend Now". :)

Find Usages

Ciężko znaleźć poprawnie działający odpowiednik w dodatkach, ale jest fajna opcja w VS. Nazywa się "View Call Hierarchy". Wystarczy ustawić swój znany skrót klawiaturowy - u mnie jest to "Shift+F12" (Find Usages z Resharpera).

Wyszukiwanie

Wyszukiwanie Resharper ma rozwiązane naprawdę koncertowo, ale jak się okazuje Visual Studio też ma się czym pochwalić. Po pierwsze mamy "Ctrl+;", aby wyszukiwać po strukturze projektu. Na dodatek mamy opcję "Navigate To". Pod tą opcję mam podpięty skrót "Ctrl+T" znany z opcji wyszukiwania po typach w Resharperze. :)

Alt+Enter

Co zastępuję Alt+Enter? Visual Studio posiada listę opcji zależnych od kontekstu kryjącą się pod skrótem klawiaturowym "Ctrl+.". Tam też podpinają się niektóre dodatki, o których pisałem wcześniej.

To chyba na razie tyle. Jak coś mi się przypomni to wrzucę update. Studio teraz działa mi o wiele sprawniej i praca jest przyjemniejsza.

Jeśli znacie jakieś inne fajne dodatki/nieznane opcje VS to dajcie znać w komentarzach. :)

PS 1

Ja wiem, że mamy tu do czynienia z sytuacją typu "coś za coś". Każdy patrzy na to pod swoim kontem. Ja wybieram wydajne środowisko. Po za tym poczytajcie co o narzędziach takich jak Resharper ma do powiedzenia Mark Seemann. Można popaść w lekką zadumę ;)

PS 2

Resharpera nie odinstalowałem całkowicie. On cały czas tu jest, ale wyłączony. Wystarczy kliknąć w ustawieniach przycisk "Suspend Now", o którym wspominałem powyżej. Może się przydać kiedy będę musiał dokonać większej modyfikacji w jakimś bardziej złożonym projekcie. :)

Prawdopodobnie po całkowitym odinstalowaniu studio działało by jeszcze lepiej i kto wie, może to kiedyś zrobię.

PS 3

Omawiane wyżej rozwiązania opisywałem w oparciu o Visual Studio 2015. Część z nich może nie być dostępna w starszych wersjach lub jest dużo bardziej uboga.