Готуємо ORM не відходячи від плити

image

Дана стаття не є закликом до екстремізму розробки велосипедів. Мета посту в тому, щоб добре зрозуміти механізм, часто його треба створити з нуля. Особливо це стосується такій ненадійній теми як ORM.

Навіщо це все?
Промислові продукти, такі як MS Entity Framework, NHibernate складні, мають величезний функціонал і по суті являють собою окрему річ в собі. Часто для отримання потрібного поведінки від таких ORM, необхідний окремий людина, що добре розбирається в тонкощах роботи таких систем, що не є добре у командному розробці.

Головний теоретичний джерело поста — книга Мартіна Фаулера «Patterns of Enterprise Application Architecture (P of ЕАА)».

Існує купа інформації про те що таке власне ORM, DDD, TTD, ГИБДД — на цьому я постараюся не зупинятися, моя мета практична. Передбачається дві частини статті, самим кінці є посилання на повний вихідний код в git hub.

Що ми хочемо отримати?
Відразу визначимося з термінами: під трекінгом будемо розуміти відстеження змін бізнес об'єктів у межах однієї транзакції, для подальшої синхронізації даних, що зберігаються
в оперативній пам'яті і вмісту бд.

Які ж є основні типи ORM?

  • За використання трекинга в .NET світі існує два типи ORM: з трекінгом змін і без трекінгу змін.
  • Щодо підходу до генерації sql-запитів: з явним використанням запитів і на основі генераторів запитів з об'єктних моделей.
Наприклад, серед відомих ORM без трекінгу і з явним використанням запитів, найяскравіший приклад — Dapper ORM від Stack Overflow. Основний функціонал таких ORM полягає в маппинге реляційної моделі бд до об'єктної моделі, при цьому клієнт явно визначає як буде виглядати його запит до бд.

Основні концепції MS Entity Framework і NHibernate у використанні трекинга і генераторів запитів з об'єктних моделей. Не буду тут розглядати переваги і недоліки цих підходів. Всі йогурти однаково корисні і істина в комбінуванні підходів.

Замовник (тобто я) захотів створити ORM з використанням трекінгу (з можливістю вимкнення) і на основі генераторів запитів з об'єктних моделей. Генерувати sql-запити будемо з лямбда — виразів мови C#, з нуля, без застосування Linq2Sql, LinqToEntities (так, тільки хардкор!).

З коробки, в MS Entity Framework є неприємність з пакетним оновленням і видаленням даних: необхідно спочатку дістати всі об'єкти бд, далі в циклі оновити\видалити і потім застосувати зміни до бд. В результаті отримуємо більше звернень до бд, ніж потрібно. Проблема описана тут. Вирішимо цю проблему у другій частині статті.

Визначимося з основними інгредієнтами
Відразу визначимося яким чином клієнтський код буде взаємодіяти з розроблюваної бібліотекою. Виділимо основні компоненти ORM які будуть доступні з клієнтського коду.
В основі концепції клієнтського взаємодії з ORM буде лежати ідея успадкування від абстрактного репозиторію. Також необхідно визначити суперклас для обмеження безлічі бізнес об'єктів, з якими буде працювати ORM.

Кожен бізнес об'єкт повинен бути унікальним у межах свого типу, тому спадкоємець повинен явно змінити свій ідентифікатор.

Порожній репозиторій і суперклас бізнес об'єкта
public abstract class Repository<T> : IRepository<T> 
where T : EntityBase,new()
{
//Пізніше реалізуємо методи
}

//суперклас бізнес об'єкта 
public abstract class EntityBase : IEntity
{
//в нащадках необхідно перевизначити ідентифікатор
public abstract int Id { get; set; }
public object Clone()
{
return MemberwiseClone();
}
}

Метод Clone() нам знадобиться для копіювання об'єкта, при трекінг, про це буде трохи нижче.

Нехай у нас буде клієнтський бізнес об'єкт, що зберігає інформацію про користувача — клас Profile.

Щоб використовувати бізнес об'єкт в ORM необхідно три кроки:

  1. Здійснити прив'язку бізнес об'єкта до таблиці в базі даних на основі атрибутів

    Клас Profile
    //Усі об'єкти повинні бути спадкоємцями суперкласу EntityBase
    [TableName("profile")]
    public class Profile : EntityBase
    {
    //Аргументом атрибута є точну назву поля в бд
    [FieldName("profile_id")]
    public override int Id { get; }
    [FieldName("userinfo_id")]
    public int? UserInfoId { get; set; }
    [FieldName("role_id")]
    public int RoleId { get; set; }
    public string Info { get; set; }
    }
    


  2. Визначити репозиторій для кожного бізнес-об'єкта

    Репозиторій профілю буде мати вигляд
    public class ProfileRepository : Repository<Profile>
    {
    //в якості аргументу базового класу передаємо назву рядка підключення
    public ProfileRepository()
    : base("PhotoGallery")
    {
    //реалізація клієнтських CRUD методів 
    }
    }


  3. Сформувати рядок підключення до бд

    Рядок підключення може виглядати так
    <connectionStrings>
    <add name="PhotoGallery" providerName="System.Data.SqlClient" connectionString="server=PC\SQLEXPRESS; database=db_PhotoGallery"/>
    </connectionStrings>

Для трекінгу змін прийнято використовувати патерн UnitOfWork. Суть UnitOfWork у відстеженні дій, що виконуються над об'єктами домену, для подальшої синхронізації даних, що зберігаються в оперативній пам'яті, з вмістом бази даних. При цьому зміни фіксуються в один момент часу — все і відразу.

Інтерфейсна частина UnitOfWork
public interface IUnitOfWork : IDisposable
{
void Commit();
}


Здавалося б, на цьому все. Але потрібно врахувати два моменти:

  • Трекінг змін повинен обслуговувати всю поточну бізнес транзакцію і повинен бути доступний для всіх бізнес об'єктів
  • Бізнес-транзакція повинна виконуватися в рамках одного потоку, тому потрібно зв'язати одиницю роботи із запущеним в даний момент потоком, використовуючи локальне сховище потоку
Якщо з потоком бізнес-транзакції вже пов'язаний об'єкт UnitOfWork, то його слід помістити саме в цей об'єкт. Крім того, з логічної точки зору одиниця роботи належить даного сеансу.

Скористаємося статичним класом Session
public static class Session
{
//локальне сховище потоку
private static readonly ThreadLocal<IUnitOfWork> CurrentThreadData = 
new ThreadLocal<IUnitOfWork>(true);
public static IUnitOfWork Current
{
get { return CurrentThreadData.Value; }
private set { CurrentThreadData.Value = value; }
}
public static IUnitOfWork Create(IUnitOfWork uow)
{
return Current ?? (Current = uow);
}
}


Якщо необхідно обійтися без трекінгу, то екземпляр класу UnitOfWork створювати не треба, рівно як і викликати Session.Create.

Отже, після визначення всіх елементів, необхідних для взаємодії з ORM, наведемо приклад роботи з ORM.

Приклад роботи з ORM
var uow = new UnitOfWork();
using (Session.Create(uow))
{
var profileRepo = new ProfileRepository();
//Виклик методів репозиторію
uow.Commit();
}



Приступаємо до готування
Все про що ми говорили раніше стосувалося public частини. Тепер розглянемо internal частина. Для подальшої розробки необхідно визначитися, що ж із себе представляє структура об'єкта для трекінгу.

Не варто плутати бізнес об'єкт і об'єкт для трекінгу:

  • Бізнес об'єкт має свій тип, тоді як трекінг об'єкт повинен бути придатний для масових маніпуляцій над безліччю бізнес об'єктів, тобто не повинен залежати від конкретного типу
  • Трекінг об'єкт є сутність існуюча в рамках конкретної бізнес-транзакції, серед безлічі бізнес об'єктів, його унікальність повинна визначатися в рамках цієї транзакції
З чого випливає, що такий об'єкт повинен володіти властивостями:

  • Унікальним в межах свого типу
  • Незмінним
По суті об'єкт для трекінгу є контейнером для зберігання бізнес-об'єктів. Як зазначалося раніше, всі клієнтські бізнес-об'єкти повинні бути предками суперкласу EntityBase і для них має бути перевизначено ідентифікатор об'єкта. Ідентифікатор забезпечує унікальність в межах типу, тобто таблиці в бд.

Реалізація контейнера об'єкта для трекінгу
internal struct EntityStruct
{
//тип об'єкта
internal Key Type { get; private set; }
internal EntityBase Value { get; private set; }
internal EntityStruct(Type key, EntityBase value)
: this()
{
Key = key;
Value = value;
}
public override bool Equals(object obj)
{
return obj.GetHashCode() == GetHashCode();
}
public bool Equals(EntityBase obj)
{
return Equals(obj);
}
public override int GetHashCode()
{
//в межах однієї бд, тип об'єкта і ідентифікатор однозначно визначають його унікальність
var code = Key.GetHashCode() + Value.Id.GetHashCode();
//хеш-код повинен бути додатним числом
return code > 0 ? code : (-1) * code;
}
}


Трекінг бізнес об'єктів

Реєстрація об'єктів для трекінгу буде відбуватися на етапі отримання цих об'єктів з репозиторію.

Після отримання бізнес об'єктів з бд, необхідна їх реєстрація об'єктів для трекінгу.
Такі об'єкти мають два стани: відразу після отримання з бд і після отримання, до фіксації змін.

Перші будемо називати «чистими», друге «брудними» об'єктами.

Приклад
var uow = new UnitOfWork();
using (Session.Create(uow))
{
var profileRepo = new ProfileRepository();
//реєструємо "чисті" об'єкти шляхом копіювання отриманих з бд, 
//вихідні вважаємо "брудними"
var profiles = profileRepo.Get(x=>x.Info = "Хороший юзер");
//змінюємо "брудні" об'єкти
foreach (var profile in profiles)
{
profile.Info = "Поганий юзер";
}
//фіксація змін
uow.Commit();
}


Важливий момент полягає в тому, що для збереження «чистих» об'єктів необхідні операції копіювання, що може згубно позначитися на продуктивності.

У загальному випадку реєстрацію об'єктів трекінгу необхідно проводити для кожного типу операцій, таким чином повинні бути об'єкти для операцій оновлення, видалення, вставки.

Потрібно врахувати, що необхідно реєструвати тільки реально змінені об'єкти, для чого необхідні операції порівняння за значенням (вище наведена реалізація структури EntityStruct з переопределенным методом Equals). У кінцевому рахунку операція порівняння, буде зведена до порівняння їх хешей.

Події реєстрації об'єктів трекінгу будуть порушуватися з функціоналу абстрактного класу репозиторію в його CRUD методи.

Реалізація функціоналу реєстрації об'єктів трекінгу
internal interface IObjectTracker
{
//для простоти наведено код реєстрації тільки для змінених і нових об'єктів об'єктів
ICollection<EntityStruct> NewObjects { get; }
ICollection<EntityStruct> ChangeObjects { get; }
//методи реєстрації 
void RegInsertedNewObjects(object sender, AddObjInfoEventArgs e);
void RegCleanObjects(object sender, DirtyObjsInfoEventArgs e);
}
internal class DefaultObjectTracker : IObjectTracker
{
//ключ словника - "брудний" об'єкт, значення - "чистий" об'єкт
private readonly Dictionary<EntityStruct, EntityStruct> _dirtyCreanPairs;
public ICollection<EntityStruct> NewObjects { get; private set; }
public ICollection<EntityStruct> ChangeObjects
{
get
{
// отримуємо змінені об'єкти
return _dirtyCreanPairs.GetChangesObjects();
}
}
internal DefaultObjectTracker()
{
NewObjects = new Collection<EntityStruct>();
//Щоб уникнути зайвих boxing/unboxing операцій реалізуємо свій EqualityComparer
_dirtyCreanPairs = 
new Dictionary<EntityStruct, EntityStruct>(new IdentityMapEqualityComparer());
}
public void RegInsertedNewObjects(object sender, AddObjInfoEventArgs e)
{
NewObjects.Add(e.InsertedObj);
}
public void RegCleanObjects(object sender, DirtyObjsInfoEventArgs e)
{
var objs = e.DirtyObjs;
foreach (var obj in objs)
{
if (!_dirtyCreanPairs.ContainsKey(obj))
{
//отримуємо "чистий" об'єкт шляхом клонування вихідного за допомогою MemberwiseClone()
var cloneObj = new EntityStruct(obj.Key, (EntityBase)obj.Value.Clone());
_dirtyCreanPairs.Add(obj, cloneObj);
}
}
}
}

Функціонал виявлення змінених клієнтом об'єктів
public static ICollection<EntityStruct> GetChangesObjects
(
this Dictionary<EntityStruct, EntityStruct> dirtyCleanPairs
)
{
var result = new List<EntityStruct>();
foreach (var cleanObj in dirtyCleanPairs.Keys)
{
if (!(cleanObj.Key == dirtyCleanPairs[cleanObj].Key))
{
throw new Exception("incorrect types");
}
if (ChangeDirtyObjs(cleanObj.Value, dirtyCleanPairs[cleanObj].Value, cleanObj.Key))
{
result.Add(cleanObj);
}
}
return result;
}
public static bool ChangeDirtyObjs(EntityBase cleanObj, EntityBase dirtyObj, Type type)
{
var props = type.GetProperties();
//цикл по кожній властивості об'єкта
foreach (var prop in props)
{
var cleanValue = prop.GetValue(cleanObj, null);
var dirtyValue = prop.GetValue(dirtyObj, null);
//якщо хоч одна властивість змінено, вважаємо об'єкт придатним для реєстрації
if (!cleanValue.Equals(dirtyValue))
{
return true;
}
}
return false;
}



Необхідно врахувати, що бізнес об'єкти однієї транзакції можуть бути з різних бд. Логічно припустити, що для кожної бд повинен бути визначений свій примірник трекінгу (класу, що реалізує IObjectTracker, наприклад DefaultObjectTracker).

Поточної транзакції потрібно заздалегідь «знати» для будь бд буде виконаний трекінг. На етапі створення екземпляра UnitOfWork, необхідно ініціалізувати екземпляри об'єктів трекінгу (екземпляр класу DefaultObjectTracker) за вказаними підключення до бд в конфігураційному файлі.

Змінимо клас UnitOfWork'а
internal interface IDetector
{
//ключ слова - рядок підключення до бази, значення - об'єкт трекінгу
Словник<string, IObjectTracker> ObjectDetector { get; }
}

public sealed class UnitOfWork : IUnitOfWork, IDetector
{
private readonly Dictionary<string, IObjectTracker> _objectDetector;
Словник<string, IObjectTracker> IDetector.ObjectDetector
{
get { return _objectDetector; }
}
public UnitOfWork()
{
_objectDetector = new Dictionary<string, IObjectTracker>();
foreach (ConnectionStringSettings conName in ConfigurationManager.ConnectionStrings)
{
//кожному підключення до бази відповідає свій примірник трекінгу
_objectDetector.Add(conName.Name, new DefaultObjectTracker());
}
}
}


Інформація про те який примірник трекінгу якій базі буде відповідати повинна бути доступна всім екземплярам репозиторію в рамках транзакції. Зручно створити єдину точку доступу в статичному класі Session.

Клас Session прийме вигляд
public static class Session
{
private static readonly ThreadLocal<IUnitOfWork> CurrentThreadData = new ThreadLocal<IUnitOfWork>(true);
public static IUnitOfWork Current
{
get { return CurrentThreadData.Value; }
private set { CurrentThreadData.Value = value; }
}
public static IUnitOfWork Create(IUnitOfWork uow)
{
return Current ?? (Current = uow);
}
//Метод повертає потрібний екземпляр трекінгу для поточної транзакції
// імені рядка підключення
internal static IObjectTracker GetObjectTracker(string connectionName)
{
var uow = Current;
if (uow == null)
{
throw new ApplicationException(" Create unit of work context and using Session.");
}
var detector = uow as IDetector;
if (detector == null)
{
throw new ApplicationException("Create unit of work context and using Session.");
}
return detector.ObjectDetector[connectionName];
}
}
}


Доступ до даних

Функціонал доступу до даних буде безпосередньо викликати методи звернення до бд. Цей функціонал буде використовуватися класом абстрактного репозиторію в його CRUD методи. У простому випадку клас доступу до даних включає в себе CRUD методи для роботи з даними.

Реалізація класу DbProvider
internal interface IDataSourceProvider : IDisposable
{
State State { get; } 
//для простоти фіксувати зміни у бд, будемо тільки для змінених об'єктів
void Commit(ICollection<EntityStruct> updObjs);
ICollection<T> GetByFields<T>(BinaryExpression exp) where T : EntityBase, new(); 
}
internal class DbProvider : IDataSourceProvider
{
private IDbConnection _connection;
internal DbProvider(IDbConnection connection)
{
_connection = connection;
State = State.Open;
}
public State State { get; private set; }
public ICollection<T> GetByFields<T>(BinaryExpression exp) where T : EntityBase, new()
{
// делегат повертає текст select-запиту за висловом exp
Func<IDbCommand, BinaryExpression, string> cmdBuilder = SelectCommandBulder.Create<T>;
ICollection<T> result;
using (var conn = _connection)
{
using (var command = conn.CreateCommand())
{
command.CommandText = cmdBuilder.Invoke(command, exp);
command.CommandType = CommandType.Text;
conn.Open();
result = command.ExecuteListReader<T>();
}
}
State = State.Close;
return result;
}
public void Commit(ICollection<EntityStruct> updObjs)
{
if (updObjs.Count == 0)
{
return;
}
// ключ - делегат повертає текст update-запиту за висловом exp
//значення - змінені об'єкти
var cmdBuilder = new Dictionary<Func<IDbCommand, ICollection<EntityStruct>, string>, ICollection<EntityStruct>>();
cmdBuilder.Add(UpdateCommandBuilder.Create, updObjs);
ExecuteNonQuery(cmdBuilder, packUpdDict, packDeleteDict);
} 
private void ExecuteNonQuery(Dictionary<Func<IDbCommand, ICollection<EntityStruct>, string>, ICollection<EntityStruct>> cmdBuilder)
{
using (var conn = _connection)
{
using (var command = conn.CreateCommand())
{
var cmdTxtBuilder = new StringBuilder();
foreach (var builder in cmdBuilder)
{
cmdTxtBuilder.Append(builder.Key.Invoke(command, builder.Value));
}
command.CommandText = cmdTxtBuilder.ToString();
command.CommandType = CommandType.Text;
conn.Open();
if (command.ExecuteNonQuery() < 1)
throw new ExecuteQueryException(command);
}
}
State = State.Close;
}
private ICollection<T> ExecuteListReader<T>(EntityStruct objs)
where T : EntityBase, IEntity, new()
{
Func<IDbCommand, EntityStruct, string> cmdBuilder = SelectCommandBulder.Create;
ICollection<T> result;
using (var conn = _connection)
{
using (var command = conn.CreateCommand())
{
command.CommandText = cmdBuilder.Invoke(command, objs);
command.CommandType = CommandType.Text;
conn.Open();
result = command.ExecuteListReader<T>();
}
}
State = State.Close;
return result;
}
private void Dispose()
{
if (State == State.Open)
{
_connection.Close();
State = State.Close;
}
_connection = null;
GC.SuppressFinalize(this);
}
void IDisposable.Dispose()
{
Dispose();
}
~DbProvider()
{
Dispose();
}
}


Класу DbProvider необхідно існуюче з'єднання з бд. Делегуємо створення з'єднання і додаткової інфраструктури окремого класу на основі фабричного методу. Таким чином створювати екземпляри класу DbProvider необхідно тільки через допоміжний клас фабрики.

Фабричний метод DbProvider'а
class DataSourceProviderFactory
{
static DbConnection CreateDbConnection(string connectionString, string providerName)
{
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new ArgumentException("connectionString is null or whitespace");
}
DbConnection connection;
DbProviderFactory factory;
try
{
factory = DbProviderFactories.GetFactory(providerName);
connection = factory.CreateConnection();
if (connection != null) connection.ConnectionString = connectionString;
}
catch (ArgumentException)
{
try
{
factory = DbProviderFactories.GetFactory("System.Data.SqlClient");
connection = factory.CreateConnection();
if (connection != null)
{
connection.ConnectionString = connectionString;
}
}
catch (Exception)
{
throw new Exception("DB connection has been failed.");
}
}
return connection;
}
public static IDataSourceProvider Create(string connectionString)
{
var settings = ConfigurationManager.ConnectionStrings[connectionString];

var dbConn = CreateDbConnection(settings.ConnectionString, settings.ProviderName);
return new DbProvider(dbConn);
}
public static IDataSourceProvider CreateByDefaultDataProvider(string connectionString)
{
var dbConn = CreateDbConnection(connectionString, string.Empty);
return new DbProvider(dbConn);
}
}


Реєстрація трекінг об'єктів повинна відбуватися в CRUD методи репозиторію, той в свою чергу делегує функціонал шару доступу до даних. Таким чином необхідна реалізація інтерфейсу IDataSourceProvider з урахуванням трекінгу. Реєструвати об'єкти будемо на основі механізму подій, які будуть порушуватися саме в цьому класі. Ймовірна нова реалізація інтерфейсу IDataSourceProvider повинна «вміти» як ініціалізувати події реєстрації на трекінг, так і звертатися до бд. В даному випадку зручно декорувати клас DbProvider.

Реалізація класу TrackerProvider
internal class TrackerProvider : IDataSourceProvider
{
private event EventHandler<DirtyObjsInfoEventArgs> DirtyObjEvent;
private event EventHandler<UpdateObjsInfoEventArgs> UpdateObjEvent;
private readonly IDataSourceProvider _dataSourceProvider;
private readonly string _connectionName;
private readonly object _syncObj = new object();
private IObjectTracker ObjectTracker
{
get
{
lock (_syncObj)
{
// отримуємо необхідний екземпляр трекінгу
return Session.GetObjectTracker(_connectionName);
}
}
}
public TrackerProvider(string connectionName)
{
_connectionName = connectionName;
_dataSourceProvider = DataSourceProviderFactory.Create(_connectionName);
// реєстрація подій трекінгу
RegisterEvents();
}
public State State
{
get
{
return _dataSourceProvider.State;
}
}
private void RegisterEvents()
{
//Використання класу коректно тільки при використанні трекинга
if (Session.Current == null)
{
throw new ApplicationException("Session has should be used. Create a session.");
};
//підписка на події трекінгу
DirtyObjEvent += ObjectTracker.RegCleanObjects;
UpdateObjEvent += ObjectTracker.RegUpdatedObjects;
}
public ICollection<T> GetByFields<T>(BinaryExpression exp) 
where T : EntityBase, IEntity, new()
{
//отримуємо вихідні об'єкти бд за допомогою екземпляра класу DbProvider
var result = _dataSourceProvider.GetByFields<T>(exp);
var registratedObjs = result.Select(r => new EntityStruct(typeof(T), r)).ToList();
//Порушуємо подія реєстрації "брудних" об'єктів
var handler = DirtyObjEvent;
if (handler == null)
return result;
handler(this, new DirtyObjsInfoEventArgs(registratedObjs));
return result;
}
public void Commit(ICollection<EntityStruct> updObjs)
{
//повністю делегуємо виконання екземпляру класу DbProvider
_dataSourceProvider.Commit(updObjs, delObjs, addObjs, packUpdObjs, deleteUpdObjs);
}
public void Dispose()
{
_dataSourceProvider.Dispose();
}
}


підсумки
Розберемося яким чином тепер будуть виглядати наші public-класи.

Як зазначалося вище, клас сховища повинен делегувати свою функціональність реалізацій інтерфейсу IDataSourceProvider'а. При ініціалізації класу репозиторію, на основі переданої в конструктор рядка підключення, необхідно створити потрібну реалізацію IDataSourceProvider'а в залежності від використання трекінгу. Також необхідно врахувати, що клас доступу до даних може в будь-який момент часу «втратити» з'єднання з бд, для чого за допомогою властивості будемо стежити за цим з'єднанням.

Клас UnitOfWork'а, як вже зазначалося раніше, у своєму конструкторі повинен створити список об'єктів класу DefaultObjectTracker по всім доступним у рядку підключення бд. Логічно, що фіксація змін має також відбуватися за всіма бд: для кожного екземпляра трекінгу буде викликатися метод фіксації його змін.

Public — класи приймуть вигляд
public abstract class Repository<T> : IRepository<T> 
where T : EntityBase, IEntity, new()
{
private readonly object _syncObj = new object();
private IDataSourceProvider _dataSourceProvider;
//c допомогою властивості "моніторимо" з'єднання з бд
private IDataSourceProvider DataSourceProvider
{
get
{
lock (_syncObj)
{
if (_dataSourceProvider.State == State.Close)
{
_dataSourceProvider = GetDataSourceProvider();
}
return _dataSourceProvider;
}
}
}
private readonly string _connectionName;
protected Repository(string connectionName)
{
if (string.IsNullOrWhiteSpace(connectionName))
{
throw new ArgumentNullException("connectionName");
}
_connectionName = connectionName;
var dataSourceProvider = GetDataSourceProvider();
if (dataSourceProvider == null)
{
throw new ArgumentNullException("dataSourceProvider");
}
_dataSourceProvider = dataSourceProvider;
}
private IDataSourceProvider GetDataSourceProvider()
{
//якщо трекінг включений створюємо екземпляр класу DbProvider'а
// інакше створюємо його ////декоровану версію - TrackerProvider
return Session.Current == null ? DataSourceProviderFactory.Create(_connectionName)
: new TrackerProvider(_connectionName);
}
public ICollection<T> Get(Expression<Func<T, bool>> exp)
{
return DataSourceProvider.GetByFields<T>(exp.Body as BinaryExpression);
}
}

public sealed class UnitOfWork : IUnitOfWork, IDetector
{
private readonly Dictionary<string, IObjectTracker> _objectDetector;
Словник<string, IObjectTracker> IDetector.ObjectDetector
{
get { return _objectDetector; }
}
public UnitOfWork()
{
_objectDetector = new Dictionary<string, IObjectTracker>();
foreach (ConnectionStringSettings conName in ConfigurationManager.ConnectionStrings)
{
_objectDetector.Add(conName.Name, new DefaultObjectTracker());
}
}
public void Commit()
{
SaveChanges();
}
private async void SaveChanges()
{
await Task.Run(() =>
{
//фіксуємо зміни в примірниках трекінгу по кожній з бд
foreach (var objectDetector in _objectDetector)
{
var provider = new TrackerProvider(objectDetector.Key);
provider.Commit(
objectDetector.Value.ChangeObjects);
}
}
);
}
}


У продовженні статті розгляну роботу з пов'язаними сутностями, генерацію sql запитів на основі дерев виразів, методи пакетного видалення/зміни даних (за типом UpdateWhere, RemoveWhere).

Повністю весь вихідний код проекту, без спрощень, лежить тут.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.