Najlepsze praktyki związane z EF v2 i architekturą dostępu do danych
Autor: Tim Mallalieu
David Hill w swoim wstępie do najnowszego dokumentu patterns & practices Architecture Guidance żartuje, że kluczem do bycia dobrym architektem jest odpowiadanie „to zależy” na większość pytań. W tym artykule wezmę sobie ten żart do serca. Jak możemy wykorzystać platformę Entity Framework w architekturze naszej aplikacji? Cóż, to zależy.
Programiści stosują dużą różnorodność filozofii programowania i stylów architektonicznych. Ten artykuł omawia trzy typowe perspektywy programowania aplikacji i opisuje, jak w każdej z nich można zastosować Entity Framework. W szczególności przyjrzę się stylowi programowania formularzocentrycznemu, modelocentrycznemu i kodocentrycznemu oraz ich relacjom z Entity Framework.
Style programowania aplikacji
Zacznę od omówienia różnych stylów programowania. To omówienie nie czyni mocnych założeń odnośnie określonych metodologii, które mogą być stosowane przy tych stylach programowania i powinienem zwrócić uwagę, że korzystałem ze stereotypów dla celów tego artykułu. Większość stylów programowania łączy elementy opisywanych przeze mnie modeli. Rysunek 1 pokazuje względne charakterystyki omawianych tutaj modeli.
Rysunek 1: Style programowania i związane z nimi kompromisy.
Formularzocentryczny. W formularzocentrycznym stylu programowania (zwanym też „formularze przed danymi”) uwaga skupia się głównie na konstruowaniu elementów wysokopoziomowego interfejsu użytkownika, które wiążą się z danymi. Środowisko tworzenia oprogramowania firmy Microsoft dla tego stylu polega często na stosowaniu techniki przeciągnij i upuść, gdzie definiujemy źródło danych, a następnie systematycznie konstruujemy zestaw formularzy, które mogą wykonywać operacje tworzenia, odczytu, aktualizowania i usuwania (CRUD) w powiązanym źródle danych. To środowisko bywa bardzo produktywne i intuicyjne dla programisty. Kosztem jest często to, że programista przyjmuje w dużym stopniu gotowce pochodzące z wykorzystywanych narzędzi i platform.
Modelocentryczny. Programowanie modelocentryczne wykracza poza podejście formularzocentryczne. W programowaniu modelocentrycznym programista definiuje model w narzędziu wizualnym lub pewnym tekstowym języku specyficznym dla danej domeny (DSL) i wykorzystuje ten model jako źródło generowania klas i bazy danych. To środowisko jest często wygodne dla programistów narzędziowych, którzy chcą wykorzystywać istniejącą infrastrukturę w celu dostarczania wartości dodanej. Jest też często przydatne dla organizacji, które chcą określać własne standardy dla swoich architektur aplikacyjnych i baz danych. Kosztem tej ścieżki historycznie były inwestycje wymagane w celu umożliwienia pełnego wykorzystania środowiska. Tak jak w przypadku stylu formularzocentrycznego, programista wykorzystujący styl modelocentryczny zwykle poświęca pewną elastyczność jako konsekwencję działania w ściślej opisanym świecie.
Kodocentryczny. W kodocentrycznym stylu programowania aplikacji prawdą jest kod. Programiści sami definiują klasy utrwalające dane. Piszą swoją własną warstwę dostępu do danych do obsługi tych klas lub wykorzystują jakąś dostępną ofertę takich klas. Główną zaletą opcji kodocentrycznej jest to, że programiści uzyskują największą elastyczność. Kwestia kosztu zależy od podejścia wybranego dla trwałych danych. Jeśli programista wybierze rozwiązanie, które pozwala mu skupić się na domenie biznesowej zamiast na infrastrukturze przechowywania danych, to całkowita korzyść z tego podejścia może być bardzo duża.
Budowanie aplikacji formularzocentrycznej
W tej części pokażę, jak zbudować bardzo prostą aplikację korzystając z podejścia formularzocentrycznego z wykorzystaniem Entity Framework. Pierwszym krokiem jest utworzenie projektu Visual Studio. Na potrzeby tego przykładu utworzyłem aplikację Dynamic Data. W Visual Studio 2010 wybieramy szablon Dynamic Data Entities Web Application, jak pokazano na Rysunku 2.
Rysunek 2: Okno dialogowe New Project w Visual Studio 2010 z zaznaczonym szablonem projektu Dynamic Data Entities Web Application.
Następnym krokiem jest określenie Entity Framework jako źródła danych dla aplikacji. Robi się to poprzez dodanie nowego elementu projektu ADO.NET Entity Data Model do projektu, jak widać na Rysunku 3.
Rysunek 3: Okno dialogowe Add New Item z zaznaczonym elementem projektu ADO.NET Entity Data Model.
Po wybraniu tego elementu projektu należy wykonać następujące trzy kroki:
- Wybrać rozpoczęcie od bazy danych.
- Wybrać bazę danych.
- Zaznaczyć tabele do zaimportowania.
W tym momencie należy kliknąć Finish (Zakończ) i zobaczyć model, który został wygenerowany z bazy danych, jak pokazano na Rysunku 4.
Rysunek 4: Domyślny model danych utworzony z bazy danych Northwind.
Gdy model jest już wygenerowany, korzystanie z niego w aplikacji Dynamic Data jest tak proste, jak skonfigurowanie formularza w celu zarejestrowania kontekstu obiektu, który został utworzony w krokach wykonanych wcześniej. W Global.asax.cs możemy zmodyfikować następujący kod, aby wskazać ten kontekst:
DefaultModel.RegisterContext(typeof(NorthwindEntities), new ContextConfiguration()
{ ScaffoldAllTables = true});
Powinniśmy być teraz w stanie uruchomić naszą aplikację i korzystać z funkcjonalnego zestawu formularzy dla naszych trwałych danych, jak pokazano na Rysunku 5.

Rysunek 5: Domyślna witryna Dynamic Data korzystająca z Entity Framework.
To ćwiczenie ilustruje najprostszy interfejs sterowany formularzami. Teraz możemy zacząć pracować nad wyglądem prezentacji i potrzebnymi zachowaniami. Platforma ASP.NET Dynamic Data wykorzystuje atrybuty CLR z przestrzeni nazw System.ComponentModel.DataAnnotations w celu zapewnienia wskazówek, jak dane powinny być przedstawiane. Na przykład możemy zmienić sposób generowania formularza poprzez dodanie adnotacji, która ukrywa określoną kolumnę. Atrybut ten jest następujący:
[ScaffoldColumn(false)]
Atrybut ScaffoldColumn wskazuje, czy platforma Dynamic Data wyświetla daną kolumnę. W przypadku, gdy tabela ma być pokazana, możemy użyć atrybutu ScaffoldColumn, aby wyłączyć wyświetlanie określonej kolumny. Ciekawym wyzwaniem w aktualnym scenariuszu jest miejsce i czas określenia atrybutu dla kolumny. W tym przypadku klasy CLR, które są wykorzystywane przez Dynamic Data, zostały wygenerowane z modelu Entity Data. Możemy dodać atrybuty do wygenerowanych klas, ale wtedy wszelkie zmiany modelu spowodują utratę tych atrybutów. Dynamic Data pozwala nam również stosować atrybuty z wykorzystaniem klas częściowych skojarzonych z generowanymi klasami, ale wtedy tracimy nieco na czytelności.
Entity Framework 4.0 zapewni model rozszerzeń, który pozwoli programistom rozszerzać narzędzia Entity Framework Designer i dodawać metadane, które będą mogły być następnie stosowane przy generowaniu kodu lub bazy danych; jednakże ta funkcjonalność nie jest dostępna w Visual Studio 2010 beta 1.
Programista Entity Framework chcący pracować z Dynamic Data może być bardzo efektywny. Może zacząć od bazy danych i opisać model odpowiednimi metadanymi, aby maksymalnie wykorzystać to środowisko. Gdy model nabierze odpowiednich kształtów, programista może skupić się na interfejsie użytkownika. Więcej informacji na temat korzystania z Dynamic Data w Entity Framework można znaleźć w oficjalnej witrynie Dynamic Data.
Przemyślenia na temat aplikacji formularzocentrycznych
Korzystanie z ASP.NET Dynamic Data i Entity Framework zapewnia bardzo efektywne środowisko do tworzenia aplikacji danocentrycznych. Jednakże aplikacje formularzocentryczne nie są naturalne dla Dynamic Data. Wiele środowisk programowania ukierunkowanych przede wszystkim na interfejs użytkownika, które pozwalają programistom budować aplikację poprzez tworzenie zestawu ekranów na podstawie danych, ma tendencję do pewnych cech wspólnych. Środowisko programistyczne zwykle opiera się na jakiejś kombinacji działań w trakcie projektowania i w trakcie wykonywania, które nakazują stosowanie danego stylu architektonicznego. Model danych często odzwierciedla kształt trwałych danych (tabel) i często dostępne jest sporo metadanych związanych z interfejsem użytkownika (jak DataAnnotations w przypadku Dynamic Data), które pomagają definiować interfejs użytkownika.
Rola Entity Framework w środowisku formularzocentrycznym polega głównie na tworzeniu abstrakcji dla źródła danych. Możliwości rozszerzeń udostępniają programiście miejsce do definiowania wszystkich metadanych modelu, które muszą być wyrażone. Możliwości mapowania pozwalają programiście zmieniać kształt klas domenowych w warstwie pośredniej w sposób deklaratywny bez konieczności zagłębiania się w kod infrastrukturalny.
Budowanie aplikacji modelocentrycznej
Programowanie modelocentryczne obiecuje, że programiści mogą deklaratywnie wyrażać model, który jest bliższy koncepcyjnej domenie biznesowej, niż problemy wykonawcze danej architektury aplikacyjnej. Dla celów tego artykułu skupiłem się na pojedynczej przestrzeni projektowej, w której definiujemy domenę oraz związane z nią metadane i z której dostarczamy klasy i dane.
W Microsoft .NET Framework 4 dostępne są liczne innowacje w narzędziach Entity Framework, które umożliwiają stosowanie środowiska modelocentrycznego. Narzędzia Entity Framework zapewniają podstawowe środowisko oraz możliwość rozszerzania ich przez programistów narzędziowych, niezależnych dostawców oprogramowania i organizacje IT. Aby zilustrować to środowisko, przestawię prostą aplikację.
Zacznę znowu od nowego projektu Dynamic Data i dodam element projektu ADO.NET Entity Data Model. Tym razem jednak zacznę od pustego modelu zamiast tworzyć model z bazy danych. Zaczynając od pustego obszaru możemy zbudować taki model, jaki chcemy. Zbuduję bardzo prostą aplikację Fitness z dwoma typami jednostek Workout i WorkoutType. Modele danych dla tych typów są pokazane na Rysunku 6.

Rysunek 6: Prosty model danych.
Gdy definiujemy tego rodzaju model w narzędziu Entity Framework Designer, nie jest tworzone mapowanie, ani definicja magazynu danych. Jednakże Entity Framework Designer pozwala teraz programistom utworzyć skrypt bazodanowy z tego modelu. Klikając prawym przyciskiem myszy obszar projektu możemy wybrać opcję Generate Database Script From Model jak pokazano na Rysunku 7, a narzędzie Entity Framework Designer wygeneruje domyślną bazę danych z tego modelu. Dla tego prostego modelu definiowane są dwie tabele. Nazwy tych tabel odpowiadają obiektom EntitySet, które są zdefiniowane w narzędziu projektowym. Domyślnie tworzona baza danych zbuduje połączenia dla relacji wiele do wielu i zastosuje schemat „jedna tabela na jeden typ” przy budowaniu tabel, które muszą obsługiwać hierarchię dziedziczenia.

Rysunek 7: Generowanie skryptu bazodanowego z modelu.
Gdy wywołamy opcję Generate Database Script from Model, nowy plik T-SQL zostanie dodany do projektu, a utworzony model zapewni metadane Entity Framework z odpowiednim mapowaniem i opisem magazynu danych. Można je zobaczyć na Rysunku 8.

Rysunek 8: Plik T-SQL wygenerowany na podstawie modelu.
Jeśli programista korzysta z Visual Studio Team Architect lub Team Suite, to może wdrożyć i uruchomić skrypt T-SQL z poziomu Visual Studio po prostu klikając plik T-SQL, a następnie naciskając F5. Pojawi się prośba o wybranie docelowej bazy danych, a następnie skrypt zostanie wykonany.
W tym samym czasie Entity Framework Designer uruchomi domyślne generowanie kodu, aby utworzyć klasy oparte na modelu, artefakty Entity Framework wymagane do opisania mapowania pomiędzy modelem a bazą danych oraz opis magazynu danych, który został utworzony. W efekcie mamy teraz silnie typowaną warstwę dostępu do danych, z której można korzystać w kontekście naszej aplikacji.
Jak dotąd zapoznaliśmy się jedynie z działaniem domyślnym. Rozszerzenia narzędzia Entity Framework Designer pozwalają nam dostosowywać wiele aspektów środowiska sterowanego modelem. Kroki związane z generowaniem bazy danych i kodu wykorzystują szablony T4, które można dostosowywać, aby określić schemat bazy danych i tworzony kod. Cały proces generowania jest przepływem zadań Windows Workflow Foundation (WF), który również można dostosowywać, a zobaczyliśmy już, jak możemy dodawać rozszerzenia do narzędzi korzystając z możliwości rozszerzania Visual Studio w oparciu o Managed Extensibility Framework. Jako przykład takiego rozszerzania przyjrzyjmy się, jak możemy zmienić krok generowania kodu w tym projekcie.
Klikając prawym przyciskiem myszy obszar projektu możemy wybrać opcję Add New Artifact Generation Item. Wybranie tego polecenia otwiera okno dialogowe, w którym możemy wybrać dowolny z zainstalowanych szablonów, żeby dodać go do projektu. W przykładzie pokazanym na Rysunku 9 zaznaczyłem szablon Entity Framework POCO Code Generator (Uwaga: Szablon POCO nie działa z Dynamic Data w Visual Studio 2010 beta 1, ale będzie działał w kolejnych edycjach). Klasy POCO (Plain Old CLR Objects) pozwalają programistom definiować jedynie te elementy, które ich interesują w swoich klasach i unikać zaśmiecania ich szczegółami implementacyjnymi z platformy przechowywania danych. W .NET 4.0 wprowadziliśmy obsługę POCO wewnątrz Entity Framework, a jednym ze sposobów tworzenia klas POCO podczas korzystania z modelocentrycznego lub danocentrycznego stylu programowania jest zastosowanie szablonu POCO. Szablon POCO jest obecnie dostępny w edycji ADO.NET Entity Framework Feature CTP 1, którą można pobrać z witryny Data Platform Development i stosować w Visual Studio 2010 beta 1.

Rysunek 9: Okno dialogowe Add New Item.
Poprzez wybranie szablonu ADO.NET EF POCO Code Generator uzyskujemy inny zestaw generowanych klas. W szczególności uzyskujemy zestaw klas POCO generowanych jako pojedynczy plik na klasę, klasę pomocniczą do wykorzystania przy zmianach powiązanych elementów oraz oddzielną klasę kontekstową. Należy zwrócić uwagę, że nic nie zrobiliśmy z modelem. Jedynie zmieniliśmy szablon generowania kodu.
Jedną z interesujących możliwości dodanych w .NET 4.0 jest opcja definiowania funkcji w kontekście Entity Data Model. Funkcje te są wyrażane w modelu i mogą być wykorzystywane w kwerendach. Pomyślmy o próbie zapewnienia metody określającej, ile kalorii jest spalanych przy danym ćwiczeniu. Nie zdefiniowaliśmy dla danego typu właściwości przechowującej spalane kalorie. Moglibyśmy odpytać istniejące typy, a następnie wyszczególnić wyniki obliczając spalane kalorie w pamięci; jednakże korzystając z funkcji definiowanych w modelu możemy przekazać to zapytanie do kwerendy bazodanowej, która jest przesyłana do magazynu danych dając tym samym efektywniejsze działanie. Funkcję możemy zdefiniować w EDMX (XML) w następujący sposób:
<Function Name="CaloriesBurned" ReturnType="Edm.Int32">
<Parameter Name="workout" Type="Fitness.Workout" />
<DefiningExpression>
workout.Duration * workout.WorkoutType.CaloriesPerHour / 60
</DefiningExpression>
</Function>
Aby pozwolić na korzystanie z tej funkcji w kwerendzie LINQ, musimy dostarczyć tę funkcję w kodzie. Opisujemy tę metodę, aby wskazać z której funkcji modelu zamierzamy korzystać. Jeśli chcemy, aby funkcja działała przy bezpośrednim wywołaniu, powinniśmy zaimplementować jej ciało. Dla celów tego ćwiczenia wzbudzimy nieobsługiwany wyjątek, ponieważ spodziewamy się korzystać z tej funkcji w formie kwerend LINQ, które będą przekazywane do magazynu danych:
[EdmFunction("Fitness", "CaloriesBurned")]
public int CaloriesBurned(Workout workout)
{ throw new NotSupportedException(); }
Jeśli chcemy następnie zbudować kwerendę do pobierania wszystkich wysokokalorycznych ćwiczeń (takich, które spalają więcej niż 1000 kalorii), to możemy napisać następującą kwerendę:
var highCalWorkouts = from w in context.MyWorkouts
where
context.CaloriesBurned(w) > 1000
select w;
Ta kwerenda LINQ jest prawidłową kwerendą, która może teraz korzystać z funkcji CaloriesBurned i być tłumaczona na polecenia T-SQL, które zostaną wykonane w bazie danych.
Przemyślenia na temat modelocentrycznego programowania aplikacji
W ułomnym przypadku, gdzie programista wykorzystuje styl oparty głównie na modelu i nie dostosuje żadnego z kroków, podejście modelocentryczne jest bardzo podobne do podejścia formularzocentrycznego. Model, z którym pracuje programista, jest modelem wyższego poziomu niż logiczny model danych, ale nadal jest to dość danocentryczny widok aplikacji.
Programiści, którzy rozszerzają swój model Entity Data w celu wyrażania większej ilości metadanych dotyczących swojej domeny i którzy dostosowują generowanie kodu i/lub bazy danych, mogą dojść do punktu, gdzie stosowane podejście zbliża się do definiowania wszystkich metadanych dla naszego programu. Jest to idealne dla organizacji IT, które chcą stosować ścisłą architekturę i zestaw standardów kodowania. Jest też bardzo przydatne dla niezależnych dostawców oprogramowania i programistów narzędziowych, którzy chcą korzystać z narzędzia Entity Framework Designer jako punktu startowego do opisywania modelu, a następnie generować z niego szersze środowisko.
Kodocentryczne programowanie aplikacji
Najlepszym sposobem opisania kodocentrycznego programowania aplikacji jest zacytowanie powiedzenia „kod jest prawdą”. W podejściu formularzocentrycznym skupiamy się na budowaniu źródła danych i modelu interfejsu użytkownika dla aplikacji. W podejściu modelocentrycznym model jest prawdą: definiujemy model, a następnie automatyczne generowanie odbywa się po obu stronach (magazynu danych i aplikacji). W podejściu kodocentrycznym wszystkie nasze zamiary są ujęte w kodzie.
Jednym z wyzwań podejścia kodocentrycznego jest kompromis pomiędzy logiką domenową a logiką infrastrukturalną. Rozwiązania Object Relational Mapping (ORM) zwykle pomagają przy podejściu kodocentrycznym, ponieważ programiści mogą skupiać się na wyrażaniu swojego modelu domenowego poprzez klasy i pozwalać ORM zająć się przechowywaniem danych.
Jak widzieliśmy w podejściu modelocentrycznym można wykorzystać klasy POCO w istniejącym modelu EDM (zarówno w podejściu ukierunkowanym na model, jak i ukierunkowanym na bazę danych). W podejściu kodocentrycznym wykorzystujemy tzw. technologię Code Only, gdzie zaczynamy od samych klas POCO bez żadnych innych artefaktów. Technologia Code Only jest obecnie dostępna w edycji ADO.NET Entity Framework Feature CTP 1, którą można pobrać z witryny Data Platform Development i wykorzystywać w Visual Studio 2010 Beta 1.
Rozważmy replikację aplikacji Fitness korzystającą tylko z kodu. Idealnie zdefiniowalibyśmy klasy domenowe w kodzie, jak pokazano na Rysunku 10.
Rysunek 10: Klasy domenowe Workout i WorkoutType.
public class Workout
{
public int Id { get; set; }
public DateTime DateTime { get; set; }
public string Notes { get; set; }
public int Duration { get; set; }
public virtual WorkoutType WorkoutType { get; set; }
}
public class WorkoutType
{
public int Id { get; set; }
public string Name { get; set; }
public int CaloriesPerHour { get; set; }
}
Aby klasy domenowe działały z Entity Framework, musimy zdefiniować wyspecjalizowany kontekst ObjectContext, który reprezentuje punkt wejścia do Entity Framework (podobnie jak abstrakcja sesji lub połączenia dla interakcji z bazą danych). Klasa ObjectContext musi definiować zbiory EntitySet, na bazie których możemy tworzyć kwerendy LINQ. Oto przykład kodu:
public class FitnessContext : ObjectContext
{
public FitnessContext(EntityConnection connection)
: base(connection, "FitnessContext")
{
}
public IObjectSet<Workout> Workouts {
get { return this.CreateObjectSet<Workout>(); } }
public IObjectSet<WorkoutType> WorkoutTypes {
get { return this.CreateObjectSet<WorkoutType>(); } }
}
W podejściu z samym kodem używana jest klasa fabryki do pobierania wystąpienia kontekstu. Ta klasa kontekstu odzwierciedla kontekst i tworzy wymagane metadane dla wykonywania. Sygnatura fabryki jest następująca:
ContextBuilder.Create<T>(SqlConnection conn)
Dla wygody możemy dodać metodę fabryki do wygenerowanego kontekstu. Zapewniamy pole statyczne dla łańcucha połączenia i statyczną metodę fabryki do zwracania wystąpień FitnessContext. Najpierw łańcuch połączenia:
static readonly string connString = new SqlConnectionStringBuilder
{
IntegratedSecurity = true,
DataSource = ".\\sqlexpress",
InitialCatalog = "FitnessExpress",
}.ConnectionString;
A oto metoda fabryki:
public static FitnessContext CreateContext()
{
return ContextBuilder.Create<FitnessContext>(
new SqlConnection(connString));
}
Teraz mamy wszystko, co trzeba, abyśmy byli w stanie korzystać z kontekstu. Na przykład moglibyśmy napisać metodę taką jak poniżej w celu odpytywania wszystkich typów ćwiczeń:
public List<WorkoutType> AllWorkoutTypes()
{
FitnessContext context = FitnessContext.CreateContext();
return (from w in context.WorkoutTypes select w).ToList();
}
Tak jak w podejściu ukierunkowanym na model wygodnie jest być w stanie wdrażać bazę danych z podejścia opartego tylko na kodzie. ContextBuilder zapewnia pewne metody pomocnicze, które mogą sprawdzać, czy baza danych istnieje, kasować ją i tworzyć.
Możemy napisać kod podobny do następującego w celu utworzenia prototypu prostego zestawu funkcjonalności demonstracyjnej korzystając z podejścia opartego tylko na kodzie:
public void CreateDatabase()
{
using (FitnessContext context = FitnessContext.CreateContext())
{
if (context.DatabaseExists())
{
context.DropDatabase();
}
context.CreateDatabase();
}
}
W tym momencie możemy skorzystać z wzorca Repository z projektowania sterowanego domeną (DDD), aby rozbudować nieco to, co widzieliśmy do tej pory. Zastosowanie zasad DDD jest obecnie typowym trendem w programowaniu aplikacji, ale nie będę tutaj próbował definiować ani omawiać projektowania sterowanego domeną (więcej informacji można znaleźć w książkach ekspertów takich jak Eric Evans (Domain-Driven Design: Tackling Complexity in the Heart of Software, Addison-Wesley, 2003) i Jimmy Nilsson (Applying Domain-Driven Design and Patterns: With Examples in C# and .NET, Addison-Wesley, 2006).
Obecnie mamy ręcznie napisany zestaw klas domenowych i wyspecjalizowaną klasę ObjectContext. Przy korzystaniu z Dynamic Data po prostu skierowaliśmy platformę na ObjectContext. Jeśli jednak chcemy rozważyć silniejszą abstrakcję naszej warstwy przechowywania danych i jeśli chcemy naprawdę ograniczyć kontrakt działań do faktycznie znaczących operacji domenowych, które powinny być dostępne, to możemy wykorzystać wzorzec Repository.
Dla tego przykładu zdefiniuję dwa repozytoria: Jedno dla klas WorkoutType i jedno dla klas Workout. Stosując się do zasad DDD powinniśmy myśleć o ogólnych podstawach, a następnie o odpowiednim modelowaniu repozytoriów. W tym bardzo prostym przykładzie skorzystałem z dwóch repozytoriów dla zilustrowania pojęć wysokopoziomowych. Rysunek 11 pokazuje repozytorium WorkoutType, a Rysunek 12 pokazuje repozytorium Workout.
Rysunek 11: Repozytorium WorkoutType.
public class WorkoutTypeRepository
{
public WorkoutTypeRepository()
{
_context = FitnessContext.CreateContext();
}
public List<WorkoutType> AllWorkoutTypes()
{
return _context.WorkoutTypes.ToList();
}
public WorkoutType WorkoutTypeForName(string name)
{
return (from w in _context.WorkoutTypes
where w.Name == name
select w).FirstOrDefault();
}
public void AddWorkoutType(WorkoutType workoutType)
{
_context.WorkoutTypes.AddObject(workoutType);
}
public void Save()
{
this._context.SaveChanges();
}
private FitnessContext _context;
}
Rysunek 12: Repozytorium Workout.
public class WorkoutRepository
{
public WorkoutRepository()
{
_context = FitnessContext.CreateContext();
}
public Workout WorkoutForId(int Id)
{
return (from w in _context.Workouts where w.Id == Id select w).FirstOrDefault();
}
public List<Workout> WorkoutsForDate(DateTime date)
{
return (from w in _context.Workouts where w.DateTime == date select w).ToList();
}
public Workout CreateWorkout(int id, DateTime dateTime, int
duration, string notes, WorkoutType workoutType)
{
_context.WorkoutTypes.Attach(workoutType);
Workout workout = new Workout() { Id = id, DateTime = dateTime, Duration = duration,
Notes = notes, WorkoutType = workoutType };
_context.Workouts.AddObject(workout);
return workout;
}
public void Save()
{
_context.SaveChanges();
}
private FitnessContext _context;
}
Jedną ze spraw wartych zauważenia jest to, że zwracanymi typami nie są IQueryable; ale List. Toczą się dyskusje, czy należy wystawiać IQueryable poza granicami warstwy przechowywania danych. Moja opinia jest taka, że wystawienie IQueryable łamie obudowanie warstwy przechowywania danych i narusza granicę pomiędzy jawnymi operacjami, które dokonywane są w pamięci i operacjami, które mają miejsce w bazie danych. Jeśli wystawimy IQueryable z repozytorium, to nie mamy pojęcia, kto utworzy kwerendę bazodanową w LINQ na wyższym poziomie stosu.
Teraz możemy wykorzystać te repozytoria, aby dodać jakieś dane do magazynu. Rysunek 13 pokazuje dwie metody, które można by wykorzystać do utworzenia jakiś danych przykładowych.
Rysunek 13: Metody budowania danych przykładowych.
public void AddWorkouts()
{
Console.WriteLine("--- adding workouts ---");
WorkoutRepository repository = new WorkoutRepository();
WorkoutTypeRepository typeRepository = new WorkoutTypeRepository();
WorkoutType squash = typeRepository.WorkoutTypeForName("Squash");
WorkoutType running = typeRepository.WorkoutTypeForName("Running");
repository.CreateWorkout(0,new DateTime(2009, 4, 20, 7, 0, 0),
60, "nice squash workout", squash);
repository.CreateWorkout(1, new DateTime(2009, 4, 21, 7, 0, 0),
180, "long run", running);
repository.CreateWorkout(2, new DateTime(2009, 4, 22, 7, 0, 0),
45, "short squash match", squash);
repository.CreateWorkout(3, new DateTime(2009, 4, 23, 7, 0, 0),
120, "really long squash", squash);
repository.Save();
}
W scenariuszu ukierunkowanym na model korzystaliśmy z funkcji definiowanych w modelu do zapewniania metody określającej, ile kalorii jest spalanych w danym ćwiczeniu, nawet jeśli typ nie ma zdefiniowanej właściwości przechwytującej spalone kalorie. W podejściu stosującym tylko kod nie mamy tutaj opcji definiowania funkcji zdefiniowanych w modelu. Możemy jednak opierać się na bazie istniejącej klasy Workout EntitySet, aby definiować kwerendę, która już obejmuje filtr dotyczący spalania kalorii, jak pokazano tutaj:
public IQueryable<Workout> HighCalorieWorkouts()
{
return (
from w in Workouts
where (w.Duration * w.WorkoutType.CaloriesPerHour / 60) > 1000
select w);
}
Jeśli zdefiniujemy tę metodę w FitnessContext, to możemy korzystać z niej w repozytorium Workout w następujący sposób:
public List<Workout> HighCalorieWorkouts()
{
return _context.HighCalorieWorkouts().ToList();
}
Ponieważ metoda w kontekście zwróciła IQueryable, to moglibyśmy dalej opierać się na jej bazie, ale dla symetrii postanowiłem po prostu zwracać wyniki jako List.
Przemyślenia na temat programowania kodocentrycznego
Metodologia kodocentryczna jest bardzo atrakcyjna dla programistów, którzy chcą wyrażać swoją logikę domenową w kodzie. Podejście kodocentryczne nadaje się dobrze do zapewniania odpowiedniego poziomu elastyczności i jasności potrzebnego do pracy z innymi platformami. Wykorzystując abstrakcje takie jak wzorzec Repository, podejście to pozwala programistom zapewniać wysoki stopień izolacji dla warstwy przechowywania danych, co pozwala aplikacji ignorować warstwę przechowywania danych.
Końcowe przemyślenia na temat stylów programowania aplikacji
Są to trzy style programowania aplikacji, z którymi często będziemy się stykać. Jak wspominano wcześniej, nie ma jednej, prawdziwej klasyfikacji tych stylów programowania. Mieszczą się one w pewnym continuum od bardzo opisowych, bardzo danocentrycznych podejść, które skupiają się na efektywności do bardzo ekspresywnych podejść kodocentrycznych.
Dla nich wszystkich można stosować Entity Framework w celu zapewniania warstwy przechowywania danych. Kierując się w stronę formularzocentrycznej i modelocentrycznej strony spektrum, jawny model oraz możliwość rozszerzania modelu i narzędzi mogą pomóc Entity Framework poprawić ogólną wydajność programisty. Po stronie kodocentrycznej ulepszenia w Entity Framework pozwalają środowisku uruchomieniowemu usunąć się na bok i stanowić jedynie szczegół implementacyjny dla usług przechowywania danych.
Tim Mallalieu jest menedżerem linii produktowej dla Entity Framework i LINQ to SQL. Można się z nim skontaktować pod adresem blogs.msdn.com/adonet.
