ADO.NET w praktyce


Załóżmy, że mamy utworzoną prostą aplikację Windows Forms, z jedną formą, na której umieścimy kontrolkę NotifyIcon do pokazania informacji, gdy ostatnia faktura uległa zmianie. Kod, który inicjuje nasłuch, może mieć postać:

  1. private void SetupDep() {

  2. SqlConnection cnn;

  3. SqlCommand cmd;

  4. SqlDependency dep;

  5. cnn = new SqlConnection(

    @"Data Source=NIEZAPOMINAJKA2\SQL2005;

    Initial Catalog=PCWKVS2005;

    Integrated Security=True");

  6. cmd = cnn.CreateCommand();

  7. cmd.CommandText = " select Invoice.Dt from dbo.Invoice";

  8. cnn.Open();

  9. dep = new SqlDependency(cmd);

  10. dep.OnChange += new OnChangeEventHandler(dep_OnChange);

  11. SqlDataReader dr=cmd.ExecuteReader();

  12. dr.Close();

  13. cnn.Close();

    }

  14. private void Form1_Load(object sender, EventArgs e) {

  15. SetupDep();

  16. }

  17. void dep_OnChange(object sender, SqlNotificationEventArgs e) {

  18. Debug.WriteLine("Event Recd");

    Debug.WriteLine("Info:" + e.Info);

  19. Debug.WriteLine("Source:" + e.Source);

    Debug.WriteLine("Type:" + e.Type);

  20. notifyIcon1.Text = "Zmiana numeru osatniej faktury:"+GetLastNumber();

  21. notifyIcon1.Visible = true;

  22. SetupDep();

  23. }

Informacja o zmianach w bazie najpierw trafia do wewnętrznej kolejki, a dopiero później jest przesyłana do klientów.

Informacja o zmianach w bazie najpierw trafia do wewnętrznej kolejki, a dopiero później jest przesyłana do klientów.

Niestety, pojawia się kilka utrudnień. Po pierwsze, po każdym zgłoszeniu zmiany w tym schemacie aplikacji trzeba ponownie zdefiniować strukturę SqlDependency. Po drugie, mechanizm tylko powiadamia o zmianach. Nie ma możliwości, by coś zwracał. Parametry zdarzenia (linia 17) określają tylko, w jaki sposób zostało zdarzenie wyzwolone. Tak więc w momencie gdy coś ulegnie zmianie i tak trzeba odpytać bazę, by wyciągnąć potrzebne informacje (w powyższym przykładzie jest zdefiniowana prosta funkcja GetLastNumber, która po prostu tworzy nowe połączenie i wybiera ostatni numer faktury).

Prześledźmy dokładnie, w jaki sposób działa procedura SetupDep, inicjująca nasłuch Server Broker.

Po utworzeniu połączenia i definicji SqlCommand (linie 5-7), otwierane jest SqlConnection i tworzone jest SqlDependency na podstawie danego polecenia. Następnie definiowana jest procedura obsługi zdarzenia - w tym wypadku dep_OnChange.

Jednak nasłuch uruchamiany jest dopiero w momencie, gdy dane polecenie jest wykonane - tak, żeby Broker wiedział co ma być monitorowane (linie 11-12). Po tych operacjach połączenie z SQL Server nie jest potrzebne i można je zamknąć.

Warto też pamiętać, że nie można stosować pełnej składni SQL. Jeżeli serwis SQL Broker nie będzie mógł monitorować danego wyrażenia, to od razu zostanie zgłoszone zdarzenie z właściwością Info równą Invalid.

W procedurze obsługi zdarzenia w tym wypadku, na wyjście debugera wyprowadzane są parametry SqlNotificationEventArgs (linie 18 i 19), po czym przy użyciu pomocniczej funkcji odczytywany jest ostatni aktualny numer faktury, który przypisywany jest następnie odpowiednim właściwościom kontrolki NotyfiIcon.

Uwaga! Monitorujemy zdarzenie, które spowoduje zmianę zawartości tabeli dbo.Invoice. Tak naprawdę nie oznacza to, że zmienił się numer ostatniej faktury - może to być np. uaktualnienie innego pola w jakimś wierszu itp. Jeżeli interesują nas tylko zmiany w konkretnym wyrażeniu, to zdarzenie zgłoszone przez SqlDependency możemy traktować tylko jako wskazówkę, że trzeba dokładnie sprawdzić, czy dana cecha uległa zmianie.

Nie należy zapomnieć o tym, że Server Broker to jest naprawdę zewnętrzny proces w stosunku do serwera SQL. Informacja o zmianie nie będzie zgłaszana natychmiast, czy w tej samej transakcji. W środku SQL zgłaszany jest komunikat do kolejki, który jest następnie przesyłany do wszystkich klientów, którzy się zapisali na dane zdarzenie.

Jednak mimo to, jest to znacznie mniej kosztowny mechanizm niż ciągłe odpytywanie serwera SQL.

Mechanizm powiadamiania o zmianach odgrywa bardzo istotną rolę w ASP.NET 2.0 - pomaga zwłaszcza przy konstrukcji stron, które prezentują dane pochodzące z bazy.

Załóżmy, że mamy stronę ASP.NET pokazującą listę faktur. Można skonstruować aplikację w taki sposób, że gdy dowolny klient wchodzi na stronę, to baza danych jest odpytywana, po czym generowana jest tabelka z zawartością. Jednak taki sposób pracy jest dosyć kosztowny i właściwie nie ma sensu, bo faktury są wprowadzane w określonych przedziałach godzinowych i nie ma ich tak dużo. Dane mogłyby być więc jakoś przechowywane, by nie obciążać za bardzo serwera SQL.

Można przyjąć, że np. pamięć podręczna strony ma "timeout" 30 min. Co jednak, gdy chcemy, aby te informacje były najświeższe?

W ASP.NET 2.0 można dodać specjalne wyrażenie, które określi ważność generowanej aplikacji. Można to zrobić albo dyrektywami na początku strony ASPX, albo użyć API:

  1. SqlCacheDependency dependency = new SqlCacheDependency("Cnn1", "Invoice");

  2. Response.AddCacheDependency(dependency);

  3. Response.Cache.SetValidUntilExpires(true);

  4. Response.Cache.SetExpires(DateTime.Now.AddMinutes(60));

  5. Response.Cache.SetCacheability(HttpCacheability.Public);

W powyższym przykładzie zakładamy, że jest dostępne połączenie o nazwie Cnn1. Monitorujemy zmiany w tabeli Invoice. Następnie określamy warunki nakładane na generowaną odpowiedź (czyli to, co jest zapisywane do strumienia Response): w linii 2 zależność od zawartości bazy, w linii 3 - określamy, że nawet jeżeli klient wyśle polecenie "Cache-Control", to serwer ma je zignorować. Ponadto dodajemy opcję, aby pamięć podręczna po 60 minutach była unieważniana. W linii 5 informujemy, że jest obojętne, kto będzie przechowywał informację o pamięci cache - czy jakiś pośredni serwer Proxy, serwer IIS, czy klient.

W ten sposób otrzymujemy optymalne połączenie. Klient aplikacji WWW ma zawsze aktualne informacje, a dodatkowo serwer SQL nie jest odpytywany, jeżeli dane bazowe nie uległy zmianie.

Warto jeszcze raz podkreślić, że każdy serwis ma sporo informacji, które są pobierane z bazy, ale nie zmieniają się zbyt często, np. kategorie towarów, pozycje menu itp. Jest wiele miejsc, gdzie SqlCacheDependency może przyjść z pomocą.