Lucrurile arata cam asa:
[Applicatie] <----> [Bussiness Layer] <----> [DataLayer] <----> [DataBase]
DataBase poate fi orice, M$ Sql, Oracle, Acces etc, insa codul o sa fie scris pentru M$ Sql.
Intr-un proiect nou, se creaza o noua clasa, cu numele dal.cs
Pentru a construi o clasa (pe care o voi numi DAL in continuare) trebuie sa importam in proiectul nostru namespace-urile System.Data.SqlClient si System.Data.SqlClient (in plus fatza de ce isi aduce default VisualStudio).
La mine arata cam asa (deocamdata):
Cod: |
using System; using System.Collections; using System.Text; using System.Data; using System.Data.SqlClient; namespace TestEMNdb { public class DAL { } } |
Primul lucru de care avem nevoie este connectionString, unde definim parametrii necesari accesari serverului unde se aftla baza de date pe care o vom folosi.
Pentru acest exemplu vom folosi urmatorul string:
Data Source=EXPEED-L006SQLEXPRESS;
Initial Catalog=Northwind;
Integrated Security=SSPI;
Unde EXPEED-L006SQLEXPRESS este numele serverului care ruleaza pe calculatorul meu, Northwind este numele bazei de date pe care o voi folosi (vine ca exemplu cu sql server) isr ultimul parametru, Integrated Security=SSPI defineste nivelul de securitate pe care il va folosi connexiunea (se vor folosi drepturile userului curent al sistemului de operare).
Locul cel mai bun de pastrare a acestui string nu este in codul clasei, insa pentru moment, o sa il pun in interiorul clasei (hard coded). Pana la sfarsitul articolului o sa ii gasim un loc mai bun.
Deasemeni, avem nevoie de 2 obiecte, SqlConnection si SqlCommand (ambele se gasesc in numele de spatiu System.Data.SqlClient).
Acum ar trebui sa ne gandim si la un constructor pentru aceasta clasa, in interiorul caruia sa initializam ceea ce avem pana acum, SqlConnection si SqlCommand:
conn = new SqlConnection(connString); // definim un obiect de tipul SqlConnection, care sa aiba grija de conexiunea cu baza de date apoi,
command = new SqlCommand(); // un obiect care sa se ocupe de ceea ce vrem sa ii transmitem bazei de date, si la urma
command.Connection = conn; // ii zicem instantei obiectului command sa foloseasca obiectul conn pentru a transmite ce are de transmis bazei de date.
Pana acum clasa noastra arata asa:
Cod: |
using System; using System.Collections; using System.Text; using System.Data; using System.Data.SqlClient; namespace TestEMNdb { public class DAL { string connString = @"Data Source=EXPEED-L006SQLEXPRESS; Initial Catalog=Northwind; Integrated Security=SSPI;"; SqlConnection conn; SqlCommand command; public DAL() { conn = new SqlConnection(connString); command = new SqlCommand(); command.Connection = conn; } } } |
Deocamdata avem o clasa care nu prea face nimic. Initializeaza niste obiecte, si cam atat. Ca sa faca ceva, sa ii punem niste metode. Ar fi necesare o metoda Open() care sa deschida coneciunea la baza de date, si o alta metoda Close(), evident, sa inchida coneciunea deschisa de metoda Open(). Acestea sunt cele mai simple metode ale acestei clase.
Cod: |
public void Open() { conn.Open(); } public void Close() { if (conn != null) conn.Close(); } |
In metoda Close() inainte de a inchide connexiunea, verific daca obiectul conn este nenul. Daca obiectul conn este null, si se incearca executarea metodei conn.Close() o exceptie va fi aruncata. Daca acest obiect nu e null, inchid conexiunea. Nu trebuie sa verific altceva, intru-cat inchiderea unei conexiuni care nu este deschisa nu arunca o exceptie. Deci executarea acestei functii este destul de sigura.
Pentru c acum putem deschide o conexiune la baza de date, sa incepem sa creem niste metode care sa ne aduca si ceva date stocate in tabelele bazei noastre de date.
Una dintre cele mai simple interogari pe care le putem face asupra tabelelor, este sa cerem o singura valoare, ca de exemplu: cat costa produsul X? sau cati munitori sunt inregistrati? Sau in ce data am facut X? Cu alte cuvinte, rezultatul pe care il primim este o valoare care reprezinta rezultatul executiei interogarii noastre asupra bazei de date si care este returnata in coloana 1 randul 1. Orice alte rezultate returnate de baza de date, sunt ignorate.
Cum facem sa primim asa ceva? Destul de simplu! Obiectul command are o metoda care face asta petru noi: command.ExecuteScalar(). Avem nevoie numai sa transmitem obiectului command interogarea sql, si el se ocupa de restul.
Cod: |
public object GetScalar(string sql) { command.CommandText = sql; command.CommandType = CommandType.Text; return command.ExecuteScalar(); } |
De retinut este ca aceasta functie returneaza un obiect. Pentru afolosi valoarea returnata trebuie facut (in mometul folosiri ei) casting la tipul de data pe care ne asteptam sa il primim.
Doar ca exemplu de folosire a acestei functii (nu este parte a obiectului DAL - intru-cat aici nu trebuie sa avem decat functii generice, nu specifice unei tabele/baze de date/actiuni) aceasta functie se foloseste asa:
Cod: |
string sql = "select count(*) from Products"; conn.Open(); int result = (int)GetScalar(sql); conn.Close(); |
Urmatoarea functie utila este o functie care sa ne permita sa face actiuni DML asupra bazei de date. Obiectul connect are inca o metoda utila numita ExecuteNonQuery(). Aceasta executa o actiune DML si returneaza numarul de randuri afectate. Aceasta functie este folosita la inserarea de date, la stergerea de date sau modificarea datelor.
Cod: |
public void ExecuteNonQuery(string sql) { command.CommandType = CommandType.Text; command.CommandText = sql; command.ExecuteNonQuery(); } |
Ca sa recapitulam, pana in acest moment am avansat destul de mult. Avem o conexiune, pe care o putem controla, si avem posibilitatea sa primim (deocamdata putine) si sa trimitem date. Destul de bine pentru cateva linii de cod.
Insa in marea majoritate a cazurilor, avem nevoie de mai mult decat o singura valoare. Avem nevoie de un rand, cateva randuri sau chiar de un tabel intreg. Una dintre cele mai simple si eficiente metode de a citi randuri returnate de o instructiune select este folosind SqlDataReader. SqlDataReader este o clasa care ne permite sa citim date intr-un mod foarte eficient. SqlDataReader nu se poate folosi pentru scrierea datelor, iar citirea se face numai prin parcurgerea rezultatelor returnate de instructiunea select numai intr-un sens: inainte! Cu alte cuvinte, daca acum citim randu 4, urmatorul rand la care avem acces pentru citire este randul 5. Pentru a ajunge la randul 3 trebuie sa o luam cu cititul de la capat, de la randul 1 (parcurgand 1 si 2 pentru a avea acces la randul 3).
De regula, datele citite cu SqlDataReader sunt salvate in obiecte care permit parcurgerea datelor in ambele sensuri (sau accesarea prin index a unui element dorit).
Clasa SqlDataReader este definita in namespace-ul System.Data.SqlClient.
Pentru a crea o instantza a SqlDataReader se foloseste metoda ExecuteReader() a unui obiect SqlCommand.
Cod: |
SqlDataReader reader = command.ExecuteReader(); |
Pentru a putea citi date folosind SqlDataReader avem nevoie de o conexiune deschisa la baza de date. De mentionat este ca aceasta conexiune va fi "ocupata" pana ce obiectul SqlDataReader o elibereaza.
Revenind la clasa noastra DAL, o sa o completam cu o functie noua, si anume una care sa returneze un obiect de tip SqlDataReader.
Cod: |
public SqlDataReader GetReader(string sql) { command.CommandType = CommandType.Text; command.CommandText = sql; SqlDataReader dataReader = command.ExecuteReader(); return dataReader; } |
Acestei functii i se transmite ca parametru interogarea sql dorita, iar setul de randuri returnate de sql este accesibil prin intermediul obiectului de tip SqlDataReader returnat. Nu uitati ca inainte de a apela aceasta functie sa deschideti o conexiune, iar la final sa inchideti conexiunea deschisa!
Un exemple de folosire al acestei functii este (nu face parte din DAL.cs - ci dintr-o clasa de tip bussinessLogic):
Cod: |
string sql = "select name from students"; con.Open(); SqlDataReader reader = GetReader(sql); while (reader.Read()) { // aici se salveaza/prelucreaza/etc datele primite } con.Close(); |
O alta metoda de a avea acces la datele returnate de o instructiune select este salvarea rezultatelor returnate in obiecte de tip DataTable sau DataSet. Descrierea acestor obiecte nu face parte din acestu tutorial. Se pot gasi tutoriale. Pe msdn sunt explicate aceste obiecte: DataTable si [url="http://msdn2.microsoft.com/en-us/librar /system.data.dataset.aspx"]DataSet[/url]
Deci, pe scurt, datele returnate dintr-o interogare sql se pot salva in obiecte de tip DataTable si DataSet. Aceste obiecte se trimit prin referintza functiei care face citirea datelor din baza de date. Aceasta functie se ocupa de "salvarea" datelor in obiect.
Cod: |
public void Fill(DataTable dt, string sql) { SqlDataAdapter adapter = new SqlDataAdapter(sql, conn); adapter.Fill(dt); } public void Fill(DataSet ds, string sql) { SqlDataAdapter adapter = new SqlDataAdapter(sql, conn); adapter.Fill(ds); } |
Avantajele folosiri DataTable sau DataSet sunt ca datele pot fi parcurse apoi fara a necesita o conexiune deschisa la baza de date. Datele pot fi parcurse in ambele sensuri, sau prin intermediul indecsilor. Deasemeni obiectele de acest tip ofera multe moduri de a prelucra datele, de a cauta date specifice etc (documentatia msdn specifica toate metodele obiectelor de acest tip). Unul din dezavantajele folosiri acestui tip de obiect este consumul de resurse. Daca trebuiesc citite date care trebuiesc parcurse in ambele sensuri, date care trebuiesc parcurse de mai multe ori, date care trebuiesc prelucrate incrucisat, etc folositi datatable sau DataSet. Daca nu, folosirea SqlDataReader este indicata (citirea pentru afisare a unui tabel, incarcarea intr-un obiect specific a anumitor valor din tabele, popularea unor obiecte de tip colectie (ArrayList, HashTable etc)).
Cam asta e ceea ce trebuie sa contina (un minim) o clasa de tip DAL. Codul intregi clase este:
Cod: |
using System; using System.Collections; using System.Text; using System.Data; using System.Data.SqlClient; namespace TestEMNdb { public class DAL { string connString = @"Data Source=EXPEED-L006SQLEXPRESS; Initial Catalog=Northwind; Integrated Security=SSPI;"; SqlConnection conn; SqlCommand command; public DAL() { conn = new SqlConnection(connString); command = new SqlCommand(); command.Connection = conn; } public void Open() { conn.Open(); } public void Close() { if (conn != null) conn.Close(); } public object GetScalar(string sql) { command.CommandText = sql; command.CommandType = CommandType.Text; return command.ExecuteScalar(); } public void ExecuteNonQuery(string sql) { command.CommandType = CommandType.Text; command.CommandText = sql; command.ExecuteNonQuery(); } public SqlDataReader GetReader(string sql) { command.CommandType = CommandType.Text; command.CommandText = sql; SqlDataReader dataReader = command.ExecuteReader(); return dataReader; } public void Fill(DataTable dt, string sql) { SqlDataAdapter adapter = new SqlDataAdapter(sql, conn); adapter.Fill(dt); } public void Fill(DataSet ds, string sql) { SqlDataAdapter adapter = new SqlDataAdapter(sql, conn); adapter.Fill(ds); } } } |
Accesul la functiile acestei clase trebuie sa se fac prin intermediul unei clase BusinessLogic, respectand principiile N-Tier.
Prin intermediul acestei clase putem executa (aproape) toate operatiile de baza asupra unei baze de date. Ea poate fi imbunatatita prin adaugarea altor functii si facilitati, precum suport pentru executarea Stored Procedures, interogarilor parametrizate samd.