Уважаемые программисты!
Не было печалиПонадобилось мне плотно поработать с ASP.NET MVC2 + Entity Framework, однако базовая функциональность работы с БД меня совсем не впечатлила, потому что нужно было каждый раз выбирать нужную коллекцию объектов из списка. Как избежать написания нескольких классов и использовать всего один — пойдет дальше речь.
Для начала определимся, что же собственно хотелось бы получиться на выходе.
Хочется удобно работать с объектами:
используя конструкции вида:
Причем, чтобы не требовалось явно указывать с каким именно набором (ObjectSet<...>) следует работать ObjectContext'у.
Так как весь мой код будет основывать на этом принципе, следует понять, что же это такое.
Рефлексия — это процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.
Вкратце — базовый класс Object имеет метод GetType(), который возвращает структуру класса (все его свойства, поля, методы). Следовательно, мы можем использовать его для вызова функция и задания значений свойствам во время выполнения, даже не зная на этапе написания кода о наличии этих полей и функций в классе.
Метод GetType() возвращает объект типа Type, который собственно нам и нужен. Пройдя по ссылке, Вы можете воочию убедиться о количестве и разнообразии методов и свойств этого объекта. Пока что нам понадобиться GetMethod() и GetProperty().
Для начала опишу принцип функционирования.
В двух словах:
При передаче объекта в угловых скобках мы уже сможем получить из него все нужные данные (в частности название, что немаловажно). Так как по умолчанию объекты в ObjectContext носят название ******Set, где ****** — имя сущности (entity) в визуальном редакторе. Воспользовавшись рефлексией, мы получаем возможность достучаться к любому набору объектов, не указывая это название в коде. Таким образом и производятся все манипуляции.
Например, во всех ObjectSet'ах содержится метод DeleteObject.
Далее, нам понадобиться сам ObjectContext (куда, собственно, мы и будет писать и получать данные):
Его выуже получили можете получить, используя мастер и визуальное редактирование объектов.
Ещё несколько строк кода просто улучшают читабельность:
Это получение имени типа Т (выше) и небольшой «макрос» получение типа ObjectContext :
В целом, вступление закончено и можно переходить ко всему коду. Далее я расскажу о не совсем очевидных кусках кода.
Тут я хотел бы объяснить некоторые моменты,про которые поленился почитать которые сразу не заработали, а также неочевидные куски кода.
Функция private object AllItemsAsObj() используется для получения самого набора объектов (ObjectSet) и доступна только внутри класса. Нужна она практически во всех случаях, так как это наш базовый объект, надо которым будут проводиться манипуляции.
Функция AddItem(T item) была написана первой.
С ходу не хотел метода Invoke метода GetMethod. Оказалось, что нужно обязательно передать в качестве параметра объект, над которым мы выполняем действия. Поставив null (как я видел в примерах на неизвестных сайтах) метод не вызывался. Обратите на это внимание!
Эта часть кода осуществляет выборку одного объекта из набора по его ID. У меня в проекте у каждого объекта есть поле ID типа Guid. Если у вас оно другое — просто замените все упоминания «ID» на свое название.
В данной функции происходит перебор всех элементов коллекции, и при совпадении значения поля ID с входным параметром происходит возврат нужного объекта.
В конце каждой функции стоит строка:
Она сохраняет все изменения, внесенные в базу данных.
Предположим, у Вас есть два класса: Unit и City.
Для объявления двух репозиториев работы с БД следует поступить так:
Всё, больше никаких манипуляций производить не нужно. У Вас уже есть готовые объекты, которые помогут в работе. При желании, можно расширить функциональность нужными Вам функциями, пронаследовав свой класс.
Требования
Для начала определимся, что же собственно хотелось бы получиться на выходе.
Хочется удобно работать с объектами:
- Добавлять.
- Удалять.
- Редактировать.
- Получать по ID.
- Получать список всех объектов.
используя конструкции вида:
Unit a = new Unit();
BaseRepository<Unit> unitRepository = new BaseRepository<Unit>();
...........
unitRepository.AddItem(a);
unitRepository.ChangeItem(a);
unitRepository.DeleteItem(a.ID);
* This source code was highlighted with Source Code Highlighter.
Причем, чтобы не требовалось явно указывать с каким именно набором (ObjectSet<...>) следует работать ObjectContext'у.
Немного теории о рефлексии
Так как весь мой код будет основывать на этом принципе, следует понять, что же это такое.
Рефлексия — это процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.
Вкратце — базовый класс Object имеет метод GetType(), который возвращает структуру класса (все его свойства, поля, методы). Следовательно, мы можем использовать его для вызова функция и задания значений свойствам во время выполнения, даже не зная на этапе написания кода о наличии этих полей и функций в классе.
Метод GetType() возвращает объект типа Type, который собственно нам и нужен. Пройдя по ссылке, Вы можете воочию убедиться о количестве и разнообразии методов и свойств этого объекта. Пока что нам понадобиться GetMethod() и GetProperty().
Собственно реализация
Для начала опишу принцип функционирования.
В двух словах:
- Получить тип переданного объекта.
- Получить из типа имя.
- Выбрать нужный набор сущностей из ObjectContext.
- «Дернуть» нужный метод (Add, Delete).
/// <summary>
/// Base repository for all sets.
/// </summary>
/// <typeparam name="T">Class from Business Model, should be EntityType</typeparam>
public class BaseRepository<T> where T : EntityObject
* This source code was highlighted with Source Code Highlighter.
При передаче объекта в угловых скобках мы уже сможем получить из него все нужные данные (в частности название, что немаловажно). Так как по умолчанию объекты в ObjectContext носят название ******Set, где ****** — имя сущности (entity) в визуальном редакторе. Воспользовавшись рефлексией, мы получаем возможность достучаться к любому набору объектов, не указывая это название в коде. Таким образом и производятся все манипуляции.
Например, во всех ObjectSet'ах содержится метод DeleteObject.
Далее, нам понадобиться сам ObjectContext (куда, собственно, мы и будет писать и получать данные):
//DataBase container
DBContainer db = new DBContainer();
* This source code was highlighted with Source Code Highlighter.
Его вы
Ещё несколько строк кода просто улучшают читабельность:
/// <summary>
/// Reflection - get name of T.
/// </summary>
private string name
{
get { return typeof(T).Name; }
}
* This source code was highlighted with Source Code Highlighter.
Это получение имени типа Т (выше) и небольшой «макрос» получение типа ObjectContext :
//Simple macros
Type dbT = typeof(DBContainer);
* This source code was highlighted with Source Code Highlighter.
В целом, вступление закончено и можно переходить ко всему коду. Далее я расскажу о не совсем очевидных кусках кода.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
namespace Chib.Lib
{
/// <summary>
/// Base repository for all sets.
/// </summary>
/// <typeparam name="T">Class from Business Model, should be EntityType</typeparam>
public class BaseRepository<T> where T : EntityObject
{
/// <summary>
/// Reflection - get name of T.
/// </summary>
private string name
{
get { return typeof(T).Name; }
}
//DataBase container
DBContainer db = new DBContainer();
//Simple macros
Type dbT = typeof(DBContainer);
/// <summary>
/// Get all items in Set as IQueryable, making easy to operate with data.
/// </summary>
/// <returns>Items in Set</returns>
public IQueryable<T> AllItems
{
get
{
return (IQueryable<T>)AllItemsAsObj;
}
}
/// <summary>
/// Get the items (ObjectSet) as Object. For internal use only.
/// </summary>
/// <returns>Object</returns>
private object AllItemsAsObj
{
get
{
PropertyInfo mi = dbT.GetProperty(name + "Set");
object obj = mi.GetValue(db, null);
return obj;
}
}
/// <summary>
/// Add item to collection and save changes.
/// </summary>
/// <typeparam name="T">The type of item</typeparam>
/// <param name="item">Added item</param>
/// <returns>True if no errors.</returns>
public bool AddItem(T item)
{
try
{
object obj = AllItemsAsObj;
obj.GetType().GetMethod("AddObject").Invoke(obj, new object[] { item });
db.SaveChanges();
return true;
}
catch
{ return false; }
}
/// <summary>
/// Get the single T item by it's ID
/// </summary>
/// <param name="id">Guid ID</param>
/// <returns>Null if nothing found.</returns>
public T GetItem(Guid id)
{
foreach (var item in AllItems)
{
if (new Guid(item.GetType().GetProperty("ID").GetValue(item, null).ToString()) == id)
return (T)item;
}
return null;
}
/// <summary>
/// Delets an item by it's ID.
/// </summary>
/// <param name="id">ID of item</param>
/// <returns>True if no errors.</returns>
public bool DeleteItem(Guid id)
{
try
{
T item = GetItem(id);
object set = AllItemsAsObj;
set.GetType().GetMethod("DeleteObject").Invoke(set, new object[] { item });
db.SaveChanges();
return true;
}
catch
{
return false;
}
}
public bool ChangeItem(T item)
{
try
{
var guid = new Guid(item.GetType().GetProperty("ID").GetValue(item, null).ToString());
T modyfying = AllItems.Single(x => x.GetType().GetProperty("ID").GetValue(null, null).ToString() == guid.ToString());
modyfying = item;
db.SaveChanges();
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Force save changes to DB.
/// </summary>
/// <returns>True if no errors.</returns>
public bool SaveChanges()
{
try
{
db.SaveChanges();
return true;
}
catch
{
return false;
}
}
}
}
* This source code was highlighted with Source Code Highlighter.
Подводные камни
Тут я хотел бы объяснить некоторые моменты,
Функция private object AllItemsAsObj() используется для получения самого набора объектов (ObjectSet) и доступна только внутри класса. Нужна она практически во всех случаях, так как это наш базовый объект, надо которым будут проводиться манипуляции.
Функция AddItem(T item) была написана первой.
С ходу не хотел метода Invoke метода GetMethod. Оказалось, что нужно обязательно передать в качестве параметра объект, над которым мы выполняем действия. Поставив null (как я видел в примерах на неизвестных сайтах) метод не вызывался. Обратите на это внимание!
/// <summary>
/// Get the single T item by it's ID
/// </summary>
/// <param name="id">Guid ID</param>
/// <returns>Null if nothing found.</returns>
public T GetItem(Guid id)
{
foreach (var item in AllItems)
{
if (new Guid(item.GetType().GetProperty("ID").GetValue(item, null).ToString()) == id)
return (T)item;
}
return null;
}
* This source code was highlighted with Source Code Highlighter.
Эта часть кода осуществляет выборку одного объекта из набора по его ID. У меня в проекте у каждого объекта есть поле ID типа Guid. Если у вас оно другое — просто замените все упоминания «ID» на свое название.
В данной функции происходит перебор всех элементов коллекции, и при совпадении значения поля ID с входным параметром происходит возврат нужного объекта.
В конце каждой функции стоит строка:
db.SaveChanges();
* This source code was highlighted with Source Code Highlighter.
Она сохраняет все изменения, внесенные в базу данных.
Пример использования
Предположим, у Вас есть два класса: Unit и City.
Для объявления двух репозиториев работы с БД следует поступить так:
BaseRepository<Unit> unitRepository = new BaseRepository<Unit>();
BaseRepository<City> cityRepository = new BaseRepository<City>();
* This source code was highlighted with Source Code Highlighter.
Всё, больше никаких манипуляций производить не нужно. У Вас уже есть готовые объекты, которые помогут в работе. При желании, можно расширить функциональность нужными Вам функциями, пронаследовав свой класс.