Kako da radite koristeći RavenDB i DataTables.Net

Photo od Brujo. Relevantno: binarna ekstrakcija baze?

Za one među vama koji, možda, ne znaju, DataTables.Net e fantastičan jQuery dodatak koji kreira prezentaciju grida podataka nudeći podršku za filtriranje i rešenja na strani servera. Da, definiše krajnje tačke web servisa, unosi podatke i sve to da bi ste vi uživali. Neki novi klinci više vole da izbegnu SQL, a jedna od najvećih stavki u bazi podataka zasnovanoj na dokumentima je RavenDB. Uz pomoć Raven-a, definišete svoje objekte domena i jednostavno ih čuvate u bazi podataka. Zatim će Raven magijom sačuvati vaše objekte kao dokumenta. Ako imate spisak: List<Customer> za čuvanje, dajte ga Raven-u koji će kreirati dokument klijenata i sačuvati ga u JSON formatu.

U ovom tekstu prikazaćemo vam kako da kombinujete dobrotu koju nudi DataTables s magijom RavenDB-a. Cilj je da se obezbedi:

  • Definisanje pojedinačne klase za indekse podataka.
  • Kontrola nad selektovanim podacima koji služe za definisanje kolone, poredak i veličinu stranica u Javascript-u; drugim rečima, DataTables će reći serveru šta treba da povuče;
  • Podrška za filtriranje s jednim poljem za pretragu, u Google stilu;
  • Iznad svega, treba da vam uštedi vreme i da vas prikaže kao heroja u očima obožavalaca.

Za nestrpljive, evo krajnjeg proizvoda

Ima mnogo tema koje ćemo pokriti, ali oni koji odmah žele svetlost na kraju tunela, mogu da vide kako će izgledati. Možda ćete prvo hteti da preuzmete rešenje da biste ga pratili uz kod. Prvo, vaš web servis ili kontroler treba da imaju sledeće:

  1. [HttpPost]
  2. public JsonResult GetTenants(string jsonAOData)
  3. {
  4. var tenantPager = new DataTablesPager(DocumentStore);
  5. var results = tenantPager.PrepAOData(jsonAOData)
  6. .FilterFormattedList();
  7.  
  8. return Json(results);
  9. }
  10.  
  11. // The core method that is used to get the data is in DataTablesPager.cs
  12. public List Filter(int pageSize, int pageIndex, string[] terms)
  13. {
  14. var targetList = new List();
  15. RavenQueryStatistics stats;
  16.  
  17. using(var session = docStore.OpenSession())
  18. {
  19. if (terms[0].Length > 0)
  20. {
  21. targetList = session.Query()
  22. .Customize(x => x.WaitForNonStaleResults())
  23. .SearchMultiple(x => x.QueryFields, string.Join(" ", terms), options: SearchOptions.And)
  24. .Statistics(out stats)
  25. .Skip(pageIndex)
  26. .Take(pageSize)
  27. .As()
  28. .ToList();
  29.  
  30. this.totalDisplayResults = stats.TotalResults;
  31.  
  32. session.Query()
  33. .Statistics(out stats)
  34. .As()
  35. .ToList();
  36. this.totalResults = stats.TotalResults;
  37. }
  38. // Code reduced for reading purposes.

Pažljivo pogledajte Generics na constructor-u – Tenant je vaš objekat domena, Tenant_Search je klasa koju će Raven koristiti da kreira indeks za preuzimanje podataka i definisanje svojstava koje možete da filtrirate na objektu. Uskoro ćemo reći nešto više o indeksiranju i o RavenDB pozadini.

Vaš Javascript će biti:

  1. var otable;
  2.  
  3. $(document).ready(function(){
  4. otable = $("#tenantTable").dataTable({
  5. "bProcessing": true,
  6. "bSort": true,
  7. "sPaginationType": "full_numbers",
  8. "aoColumnDefs": [
  9. { "sName": "Name", "aTargets": [0], "bSortable": true, "bSearchable": true },
  10. { "sName": "Agent", "aTargets": [1], "bSortable": true, "bSearchable": true },
  11. { "sName": "Center", "aTargets": [2], "bSortable": true, "bSearchable": true },
  12. { "sName": "StartDate", "aTargets": [3], "bSortable": true, "bSearchable": true },
  13. { "sName": "EndDate", "aTargets": [4], "bSortable": true, "bSearchable": true },
  14. { "sName": "DealAmount", "aTargets": [4], "bSortable": true, "bSearchable": true }
  15. ],
  16. "oLanguage": {
  17. "sSearch": "Search all columns:"
  18. },
  19. "aaSorting": [[1, "asc"]],
  20. "iDisplayLength": 7,
  21. "bServerSide": true,
  22. "sAjaxSource": "GetTenants",
  23. "fnServerData": function (sSource, aoData, fnCallback) {
  24.  
  25. var jsonAOData = JSON.stringify(aoData);
  26.  
  27. $.ajax({
  28. //dataType: 'json',
  29. contentType: "application/json; charset=utf-8",
  30. type: "POST",
  31. url: sSource,
  32. data: "{jsonAOData : '" + jsonAOData + "'}",
  33. success: function (msg) {
  34. fnCallback(msg);
  35. },
  36. error: function (XMLHttpRequest, textStatus, errorThrown) {
  37. alert(XMLHttpRequest.status);
  38. alert(XMLHttpRequest.responseText);
  39.  
  40. }
  41. });
  42. }
  43. });
  44.  
  45. otable.fnSetFilteringDelay(1000);
  46. });

Zapravo, duži je nego .Net!!! U nastavku ćemo objasniti šta to znači.

Dobijanje podataka i pretraga uz pomoć RavenDB-a

Pretpostavljamo da imate instaliran RavenDB, da imate pristup, da znate kako da sačuvate svoje objekte i da pokrećete pretrage uz pomoć LINQ-a. Postoji nekoliko dobrih tutorijala za Raven kao što su Rob Ashton’s video introduction i brief overview by Sensei (to sam ja). Koncentrisaćemo se na Raven-ove inherentne sposobnosti za pretragu teksta i oslonićemo se na njegove ugrađene mehanizme paginacije koji će nam pomoći u ostvarivanju ciljeva. Iako Raven pruža velike mogućnosti, ipak nije SQL. Mada, ono što znate o LINQ-u i SQL-u istovremeno vam može dosta pomoći, ali i otežati. Nešto kasnije, govorićemo i o tome.

Prvo, RavenDB izgrađen je na pretraživaču Lucene.Net. To je baza podataka bez shema, tako da ćemo prvo morati da identifikujemo kako želimo da preuzmemo podatke, pošto indeksi omogućavaju super-brzo preuzimanje podataka. Raven Indeksi smanjuju potrebu za velikim cpu ciklusima obrade upita jer je indeks izgrađen od dokumenata i obrađuje se kao pozadinska operacija. Ova operacija je asinhrona i vrši je Lucene. Bez ovakvog pristupa, svaki upit traži kompletno skeniranje svih dokumenata. Očajno sporo skeniranje. Dok indeksi definišu, Raven ih tiho ažurira u trenutku kreiranja novih dokumenata. Dakle, zašto je to važno? To što mislite da radimo u LINQ-u:

  1. var search = "Bonus";
  2. var steps = session.Query()
  3. .Customize(x => x.WaitForNonStaleResults())
  4. .Where(x => x.State.StartsWith(search) || x.WorkflowName.StartsWith(search))
  5. .ToList();

u stvari se prevodi na Lucene syntaksu. Dakle, na kraju dobijamo State:Bonus ILI WorkflowName:Bonus. Iako je jednostavno napisati upit koji obuhvata sva svojstva objekta, kad biste imali objekat sa 15 osobina, da li biste stvarno poželeli da kreirate čudovišnu izjavu sa gomilom ||? Naravno da ne! Ako bacite pogled na TestSuite projekat izvornog koda, videćete nekoliko primera upotrebe čistih LINQ pretraga. Obratite pažnju na metod: “CanFilterAccrossAllStringProperties” i videćete kuda sve ovo vodi.

Hoćemo da budemo kao Fonzie. Kakav je bio Fonzie? Correctomundo – bio je cool. Bilo bi dobro znati koja je svojstva imao objekat domena i napraviti filter za ta svojstva. Drugim rečima, pomoglo bi nam kad bismo napisali kod koji bi izgledao ovako:

  1. var propertyFilterSteps = session.Query()
  2. .Customize(x => x.WaitForNonStaleResults())
  3. .Where(AllStringPropertiesFilter(search, "Answer,AnsweredBy,Id,Participants,WorkflowName,WorkflowType,"))
  4. .ToList();

Ovde koristimo Expression<Func<T>> za popunjavanje razgraničene liste naziva svojstava i uz pomoć male LINQ pretrage na osnovu klase Step, generisana Lambda može obraditi filter. To je metod testa “CanFilterAccrossAllStringProperties”. Odlično je radio dok se nije ukazala potreba da ubacimo atribute: DateTime. Kod je uključen u projekat, pa ga možete pogledati ovde.

Kako postižemo cilj da dobijemo samo jedno tekstualno polje koje će odgovarati pretrazi za sve vrste osobina? Kad ukucate: “Spock 2010″, da li će se pojaviti rezultati koji odgovaraju pretragama za obe vrednosti: i za “Spock” i za “2010″? Vratimo se na Raven određivanje indeksa mapiranjem. Koje osobine želite da uključite u indekse i kako hoćete da Raven, odnosno Lucene, raščlani tekst da bi se vrednosti poklopile s indeksima? Raven obezbeđuje klasu pod nazivom: “AbstractIndexCreationTask” gde definišete funkcije Map/Reduce da biste kreirali indeks. Drugim rečima, sami birate koje će osobine biti uključene u indeks. Možete uskladiti izlaz mape sa svojim potrebama. Sve to dešava se u klasi koju ćemo nazvati ReduceResult i pretražićemo tu klasu na osnovu svojstava. Hoćemo da kažemo Raven-u da uzme značajne osobine i da od njih napravi indekse u formatu koji odgovaraju našim terminima. Dakle, kreiraćemo sledeći indeks koji će nam dozvoliti da filtriramo bilo koji. Ovo ćete naći u glavnom folderu u Step_Search.cs

  1. public class Step_Search : AbstractIndexCreationTask
  2. {
  3. public class ReduceResult
  4. {
  5. public string[] QueryFields { get; set; }
  6.  
  7. // ... code eliminated for reading purpose
  8. }
  9.  
  10. public Step_Search()
  11. {
  12. Map = steps =>
  13. from step in steps
  14. select new
  15. {
  16. QueryFields = new [] { step.State, step.Answer, step.AnsweredBy, step.WorkflowName,
  17. step.Created.ToShortDateString(), step.Created.Year.ToString(),
  18. step.Created.Month.ToString() + "/" + step.Created.Year.ToString()},
  19. DateCreated = step.Created,
  20. WorkflowName = step.WorkflowName,
  21. State = step.State
  22. };
  23. Indexes.Add(x => x.QueryFields, FieldIndexing.Analyzed);
  24.  
  25. // ... more code eliminated for reading purposes

Stoga, kreirali smo indeks koji ima niz stringova. Ovaj niz sadrži spisak osobina s kojima ćemo da poredimo tekst. Raven ima metod nazvan Search koji će uporediti “StartsWith” stil sa svakim objektom u nizu. Poziv je: .Search(x => x.QueryFields, “niz za pretragu”). Ako pogledate indeks, videćete da smo uradili samo nekoliko dodatnh operacija sa datumima: za jedan, kreirali smo prikaz u nizu u ShortDate formatu. Dakle, kada korisnik zna tačan datum, može da ga unese i Pager će ga naći. Pošto želimo da što više pojednostavimo stvari, kreirali smo nizove u formatu mm/yyyy, tako da je korisnicima olakšano filtriranje čak i ako znaju samo mesec i godinu stavke koje traže. "Mislim da je to bilo u aprilu prošle godine ..." Ovo je velika pomoć za korisnike koji se ne sećaju detalja jer im omogućava da brzo nađu ono što traže.

Samo još jedna stvar pre nego što pređemo na DataTables. Raven obezbeđuje metod pretrage koji funkcioniše sa IRavenQueryable kolekcijom. Pogledajte DataTablesPager.Filter method i videćete SearchMultiple method koji je ubačen za pretragu višestrukih termina. Drugim rečima, IRavenQueryable će prvo tražiti reč “Spock”, a zatim “2010″. Phillip Haydon je smislio taj pristup koji radi i na parcijalnim odgovorima, kao što Lucene obezbeđuje pravu sintaksu. U suprotnom, dobili biste neke čudne rezultate jer bi Lucene analizator teksta na osnovu vašeg unosa i tokena dobio rezultat “spoc 201″ i ne bi vam prikazao ono što ste tražili. Sjajan pristup Haydon-a premošćava taj jaz uz pomoć metoda ekstenzije koji vezuje termine pretrage. Opis ćete naći u klasi RavenExtensionMethods.cs. U osnovi, on označava string koji pretražujete, kreira niz i pojedinačne pozive Search() metoda za svakog člana u nizu. Omogućava nam da filtriramo parcijalna podudaranja kao što je, npr. “spoc 201″. Isprobajte ovo na stranici Tenant.aspx u okviru WebDemo rešenja i vidite kako funkcioniše.

Jeste li već ostali bez daha? Hajde da vidimo DataTables.Net!!!

Ostali ste bez daha? Dobro! Čeka nas još malo posla! Hajde da vidimo kako radi DataTables.Net? DataTables koriste sledeće parametre pri obradi podataka na strani servera:

Podaci poslati serveru:

TipNazivInfo
intiDisplayStartPrikazuje polaznu tačku
intiDisplayLength Broj zapisa za prikazivanje
intiColumns Broj prikazanih kolona (korisno za pretragu podataka u pojedinačnim kolonama)
string sSearchOpšte polje pretrage
booleanbEscapeRegexRezultat opšte pretrage se poklapa ili ne poklapa
booleanbSortable_(int)Indikator koji pokazuje da li je kolona označena za razvrstavanje na strani klijenta ili ne
booleanbSearchable_(int)ndikator koji pokazuje da li je kolona označena za pretragu na strani klijenta
stringsSearch_(int)Filter pojedinačne kolone
boolean bEscapeRegex_(int)Filter pojedinačne kolone se poklapa ili ne
intiSortingCols Broj kolona za sortiranje
intiSortCol_(int) Kolona koja se sortira (moraćete da dekodirate ovaj broj za svoju bazu podataka)
string sSortDir_(int)Smer sortiranja opadajući ili rastući – “desc” ili “asc”. Zapazite da je prefiks ove promenljive pogrešan u 1.5.x kad se koristi iSortDir_(int)
stringsEchoPodatak za formiranje DataTables

Odgovor od servera

U odgovoru na svaki zahtev za podacima koji DataTables pošalje serveru, očekuje se formiran JSON objekat sa sledećim parametrima.

Tip NazivInfo
intiTotalRecordsUkupni zapisi pre filtriranja (tj. ukupan broj zapisa u bazi podataka)
intiTotalDisplayRecordsUkupni zapisi posle filtriranja (tj. ukupan broj zapisa posle filtriranja – ne samo broj zapisa koji su se vratili u ovom skupu rezultata)
stringsEchoNeizmenjena kopija sEcho-a poslatog od strane klijenta. Ovaj parametar će se menjati sa svakim potezom (u osnovi, to je obračun) – pa je njegova primena veoma važna. Ne zaboravite da se iz bezbednosnih razloga preporučuje podešavanje tog parametra na ceo broj da bi se sprečili XSS napadi.
stringsColumnsIzborni – ovo je niz naziva kolona razdvojenih zarezima (koriste se u kombinaciji sa sName) što omogućava da DataTables reorganizuju podatke na strani klijenta ako je potrebno zbog prikaza
array array mixedaaData Podaci u 2D nizu

DataTables će objaviti AOData objekat. Klasa DataTablesPager.cs će raščlaniti ovaj objekat metodom PrepAOData. Odgovoran je za određivanje karakteristika koje pretražujete, sortiranje podataka, veličinu stranica kao i kriterijuma za filtriranje. Pošto koristimo generiku, PrepAOData ne brine o tome koje objekte upotrebljavate u domenu budući da je dizajniran za čitanje karakteristika i njihovo povezivanje prema listi podataka koje DataTables šalje našoj aplikaciji na serveru. Da naglasimo, naš cilj je da DataTables odlučuju o tome šta traže i, sve dok obavljamo svoj posao posle kreiranja indeksa, treba da budemo veoma fleksibilni.

Hajde da, još jednom, pogledamo Javascript:

  1. "aoColumnDefs": [
  2. { "sName": "Name", "aTargets": [0], "bSortable": true, "bSearchable": true },
  3. { "sName": "Agent", "aTargets": [1], "bSortable": true, "bSearchable": true },
  4. { "sName": "Center", "aTargets": [2], "bSortable": true, "bSearchable": true },
  5. { "sName": "StartDate", "aTargets": [3], "bSortable": true, "bSearchable": true },
  6. { "sName": "EndDate", "aTargets": [4], "bSortable": true, "bSearchable": true },
  7. { "sName": "DealAmount", "aTargets": [4], "bSortable": true, "bSearchable": true }
  8. ],

U aplikaciji na strani servera imamo Tenant_Search.cs koji kreira indeks sa sledećim karakteristikama: Name/Naziv, Agent/Agent, Center/Centar, StartDate/Datum početka, EndDate/Datum završetka i DealAmount/Iznos. Javascript je način da DataTables kažu: “Hej, serveru, vrati mi ove podatke u obliku niza parova vrednosti i, uzgred, uzmi ove kolone sa podacima i upotrebi ih kad mi isporučiš podatke.” Na strani servera nije nas briga kojim će redom biti poređane kolone jer server pretpostavlja da će se DataTables pobrinuti za to. DataTables uz zahtev šalje i naziv parova vrednosti. Server ga uzima, prosleđuje pretraživaču i zatim ga DataTables sažvaću. Možete da promenite Javascript i ostavite na miru svoju aplikaciju servera sve dok se držite polja koja ste uključili u indeks. Baš kao i Fonzie, budite cool!

Činjenica da će Raven odraditi straničenje umesto nas zvuči još bolje! Ima ugrađen limit od 128 dokumenata odjednom. S obzirom na veliku brzinu preuzimanja, sve te karakteristike savršeno nam odgovaraju. Ako pogledate Raven konzolu za svaku stranicu koju preuzimate, videćete koliko je kratko vreme preuzimanja. Zapamtite, nije potrebno mnogo vremena za obradu upita jer je indeks već odradio teži deo posla. Primer za to je Tenants.aspx u WebDemo solution koji straniči i filtrira više od 13,000 dokumenata. To je brzina munje.

Zar vam glava još nije eksplodirala?

Treba svariti puno podataka. Izvorni kod je ovde, zajedno sa sredstvima za kreiranje 13,000 dokumenata koje možete upotrebiti za testiranje. Imajte u vidu da ćete morati da uzmete Raven assemblies/packages kroz NuGet. U suprotnom, preuzećete oko 36 MB. Radimo na odgovorima za zahteve sortiranja i nadam se da će vas zanimati kako se taj rad odvija, o čemu ćemo vas obavestiti u budućim tekstovima. Otkrili smo veoma moćan, ali jednostavan način da prikažemo podatke iz baze podataka dokumenata uz malo angažovanje pozadinske aplikacije.

Igrajte se kodom. Jedini način da poboljšamo rad je da primimo konstruktivne kritike, prilagodimo se novim idejama i da rastemo! Neki od eksperimenata koji nisu uspeli takođe su uključeni u test, tako da možete videti kako se napredak odvijao. Obeleženi su kao neuspesi, pa ćete moći da se koncentrišete na testiranje klase DataTablesPager. Ipak, i ti neuspesi su zanimljivi i mogu vam pomoći da vidite kako smo došli do rešenja. Takođe, kad prvi put pokrenete web site, Global.ascx će potražiti ostale zapise o testiranju i kreirati ih. To će malo potrajati, ali ako hoćete, sačekajte delove označene za vas, tako da ih možete komentarisati i raditi ono što ste zamislili. Uživajte.


David Robbins je ActiveEngine Sensei i objavljuje svoje sumanute ideje o web-u i programiranju na .Net-u, o Elvisu i zenu produktivnosti na blogu ActiveEngine.Net. Vreba u spoljašnjem domenu otvorenog softvera i voli da pravi mešavine najboljih rešenja koristeći jQuery, KnockoutJs i C#.