Spojrzenie w DAL

Po stworzeniu bazy danych, nadszedł wreszcie czas na pierwsze linie kodu. Na początek – generowanego automatycznie.

Object-relational mapping
Wśród technologii wybranych do realizacji projektu wymieniłem LINQ to SQL – i tego właśnie narzędzia ORM będę używał.
(Dygresja: spotkałem się z dwoma tłumaczeniami nazwy Language-Integrated Query na język polski: zintegrowany język zapytań, język zapytań zintegrowanych. Czy poprawnym tłumaczeniem nie byłoby: zapytanie (zapytania) zintegrowane z językiem?)
Aby wprawić Linq2Sql w ruch, należy stworzyć odpowiedni DataContext – klasę, która będzie realizowała połączenie z bazą danych. W tym celu wysłużę się programem SqlMetal. Jest to narzędzie generujące potrzebny mi kod na podstawie pliku bazy danych. Jako że nie trawię aplikacji konsolowych, korzystam z SqlMetal Builder (interfejsu graficznego dla SqlMetal). Chwała automatyzacji, ponad trzy tysiące linii kodu napisały się same w kilka sekund.

Repository Pattern
Kolej na kodowanie własnoręczne. W tworzeniu warstwy dostępu do danych wykorzystam wzorzec projektowy repository. Opiera się on na klasach reprezentujących repozytorium dla każdego obiektu biznesowego. Repozytoria powinny udostępniać standardowe operacje CRUD, a także inne, właściwe dla poszczególnych obiektów biznesowych, operacje (np. selekcji).
Tworzę zatem bazowy interfejs repozytorium:

namespace InvoiceInvoker.Logic.RepositoryInterfaces
{
	public interface IRepository<T>
	{
		T GetById(int id);
		void Add(T item);
		void Remove(int itemId);
		void Update(T item);
	}
}

Niektóre obiekty wymagają rozszerzenia tego interfejsu. Przykładowo, faktury:

public interface IInvoiceRepository : IRepository<Invoice>
{
	List<Invoice> GetAll();
	List<Invoice> GetByStatus(string status);
	List<Invoice> GetByDate(DateTime dateFrom, DateTime dateTo);
	List<Invoice> GetByCustomer(string companyName);
	List<Invoice> GetByProduct(string productName);
	List<Invoice> GetByValue(decimal valueFrom, decimal valueTo);
	List<Invoice> GetByExpression(Func<Invoice, bool> expression);
}

Implementacja repozytoriów
Korzystanie z klasy DataContext rodzi pytanie: jaki powinien być czas życia pojedynczego jej obiektu? Jedna instancja na cały projekt, na jedno repozytorium, czy na jedną operację? MSDN podaje:
In general, a DataContext instance is designed to last for one “unit of work” however your application defines that term. A DataContext is lightweight and is not expensive to create. A typical LINQ to SQL application creates DataContext instances at method scope (…)
Mam zatem odpowiedź. Mogę przejść do implementacji poszczególnych repozytoriów. Przykładowe metody repozytorium faktur:

public class InvoiceRepository : InvoiceInvoker.Logic.RepositoryInterfaces.IInvoiceRepository
{
	private int registeredSellerId;
	private string connectionString;
	private DataLoadOptions loadOptions;

	public InvoiceRepository(int registeredSellerId)
	{
		this.registeredSellerId = registeredSellerId;

		connectionString = ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString;

		loadOptions = new DataLoadOptions();
		loadOptions.LoadWith<Invoice>(x => x.Customer);
		loadOptions.LoadWith<Invoice>(x => x.Products);
		loadOptions.LoadWith<Invoice>(x => x.Seller);
	}

	public List<Invoice> GetAll()
	{
		using (MainDataContext dataContext = new MainDataContext(connectionString))
		{
			dataContext.LoadOptions = loadOptions;
			return dataContext.Invoices.Where(i => i.RegisteredSellerId == registeredSellerId).ToList();
		}
	}

	// ...

	public List<Invoice> GetByProduct(string productName)
	{
		using (MainDataContext dataContext = new MainDataContext(connectionString))
		{
			dataContext.LoadOptions = loadOptions;
			var query = from invoice in dataContext.Invoices
						where invoice.RegisteredSellerId == registeredSellerId &&
							  invoice.Products.Any(x => x.Name.StartsWith(productName))
						orderby invoice.CreationDate descending
						select invoice;
			return query.ToList();
		}
	}

	public List<Invoice> GetByExpression(Func<Invoice, bool> expression)
	{
		using (MainDataContext dataContext = new MainDataContext(connectionString))
		{
			dataContext.LoadOptions = loadOptions;
			var mainQuery = from invoice in dataContext.Invoices
						where invoice.RegisteredSellerId == registeredSellerId
						orderby invoice.CreationDate descending
						select invoice;
			return mainQuery.Where(expression).ToList();
		}
	}

	// ...

	public void Add(Invoice item)
	{
		using (MainDataContext dataContext = new MainDataContext(connectionString))
		{
			dataContext.Invoices.InsertOnSubmit(item);
			dataContext.SubmitChanges();
		}
	}

	// ...
}

Zaznaczone linie pokazaują łatwość, wygodę i swobodę, cechujące proces konstruowania zapytań z użyciem LINQ i Lambda Expressions (na szczególne podkreślenie zasługuje linia trzydziesta siódma). Tonę swobody zapewnia również metoda GetByExpression, pozwalająca uzyskać listę faktur spełniających zupełnie dowolne kryteria… Chociaż może “dowolne” to za dużo powiedziane – wszystkie metody grupowej selekcji zwracają tylko faktury przypisane do konkretnego sprzedawcy (pole registeredSellerId).
Wspomnę jeszcze, że nie każda tabela bazy danych doczekała się własnego repozytorium. Nie przewiduję możliwości bezpośredniego grzebania patykiem w wątpiach tabel Sellers, Customers i Products (patrz poprzedni post), dlatego nie stworzyłem ich repozytoriów (choć być może w przyszłości pojawią się jakieś klasy mające dostęp do tych tabel).

Tak oto powstała pierwsza warstwa aplikacji. Autor lirycznie odchodzi w siną dal stronę zachodzącego słońca.

PS. Upubliczniłem wreszcie projekt na CodePlex. Dodałem też listy Done i To do na stronie projektu. Jak pokazuje lista Done, do kodowania mam większy zapał, niż do blogowania – zaległości postaram się jednak w nadchodzących dniach nadrobić, więc zapraszam do śledzenia bloga!