Работа с базой данных - QualitySolution/QSProjects GitHub Wiki
В основе работа с базой данных построена на ORM NHibernate(https://nhibernate.info/).
Добавление типового объекта(документа) в приложении обычно выглядит следующим образом:
- Добавляем класс доменной модели(Domain), описывающий объект(документ)
- Меняем базу в зависимости от подхода:
- Только скрипты обновления - Вносим изменения в скрипт создания базы и в текущий или новый скрипт обновления. В зависимости от того что сейчас с релизе.
- Модель базы в Workbanch - В модель базы данных(обычно файл base\Factory.mwb) добавляем таблицу и колонки. После добавления можно синхронизировать с БД разработки.
- Добавляем маппинг объекта на базу(HibernateMapping), то есть связи объекта с полями.
Все, можно подгружать объект и использовать его в коде диалогов.
Для сессией NHibernate у нас имеется собственная надстройка, реализующая некоторую дополнительную функциональность в инфраструктуре проекта. В целом, UnitOfWork равен Session в NHibernate. И сама концепция наличия единицы работы соответствует сессии NHibernate. С архитектурной точки зрения это одно и тоже.
Концепция единицы работы подразумевает, что пользователь обычно работая с программой, работает в какой-то одной или нескольких(переключаясь между ними) единицах работы. В большинстве случаев, единица работы = диалог пользователя. Но это не всегда, так например, какой-то самодостаточный алгоритм(задача) запускаемый диалогом или другим компонентом программы, может содержать свою единицу работы(то есть сессию к базе данных).
В общем смысле единицу работы можно представить в виде облачка подмножества(несколько из всех имеющихся), загруженных в конкретный диалог объектов базы данных. Например, если диалог редактирует документ заказа, в единицу работы скорей всего будут загружены следующие объекты: заказ, строки заказа, клиент, склад отгрузки и т.п. Работа со всеми этими объектами происходит одновременно и сохранятся в базу данных они должны одновременно. То есть, если при изменении нескольких объектов, их все нужно будет сохранить. Сохранить отдельно только один объект из сессии не получится, не отвязывая его от сессии. Это позволяет поддерживать целостность данных при сохранении.
При сохранении первого объекта через UoW.Save(obj), UnitOfWork открывает новую транзакцию в базе данных, сохранение всех остальных объектов помещается в эту транзакцию. Транзакцию нужно обязательно закрывать вызывая Uow.Comit(). Только в этот момент данные попадают в базу данных окончательно. Обратите внимание, что для UnitOfWork открытых с корневым объектом вызов Save(root) на корневом объекте или вызов UoW.Save() без аргумента, сохраняют корневой объект и закрывают транзакцию.
UnitOfWork может быть двух видов, обычный непривязанный к каком-либо корневому объекту, и с корневым объектом, как правило открытым для диалога редактирования корневого объекта. Предполагается, что все объекты в UnitOfWork второго типа загружены только чтобы работать с корневым объектом и как-то с ним связаны.
UnitOfWork помогает реализации нескольких механизмов:
UnitOfWork необходимо создавать через фабрику IUnitOfWorkFactory. Пример создания и использования локального UnitOfWork:
using(var uow = UnitOfWorkFactory.CreateWithoutRoot()) {
var ids = nodes.Select(n => n.Id).ToArray();
uow.GetAll<Post>()
.Where(post => ids.Contains(post.Id))
.UpdateBuilder()
.Set(post => post.CostCenter, costCenter)
.Update();
}
Как видите UnitOfWork необходимо всегда закрывать вручную с помощью Dispose() или используя using. Если не закрыть UnitOfWork он возможно зависнет в памяти так как используется глобальными перехватчиками.
В диалогах редактирования конкретных сущностей базирующихся на EntityDialogViewModelBase, UnitOfWork создается с помощью специального класса IEntityUoWBuilder передаваемого диалогу из вне. Этот класс по сути просто хранит настройку нужно ли диалогу создать новый объект или подгрузить имеющийся из базы.
Часто в диалоге мы в качестве зависимостей хотим получать различные классы или сервисы которые могут иметь возможность обращаться к базе данных, а значит им нужен наш UnitOfWork диалога. Но мы им не может передать в конструктор так как он еще не создан на момент создания этих классов. При этом нам бы не хотелось настраивать каждый из этих классов отдельно в конструкторе диалога, передавать ему в ручную UnitOfWork. Для упрощения отложенной передачи UnitOfWork в такие классы служит UnitOfWorkProvider. Экземпляр этого класса создается один на скоуп и будет передан всем классам кто его запросил в этом скоуп. При создании UnitOfWork в диалоге будет один раз передан в класс провайдера, тем самым мы автоматически распространим UnitOfWork во все классы рабочего скоупа.
#region База
builder.RegisterType<DefaultUnitOfWorkFactory>().As<IUnitOfWorkFactory>();
builder.RegisterType<DefaultSessionProvider>().As<ISessionProvider>();
builder.Register(c => new MySqlConnectionFactory(Connection.ConnectionString)).As<IConnectionFactory>();
builder.Register<DbConnection>(c => c.Resolve<IConnectionFactory>().OpenConnection()).AsSelf().InstancePerLifetimeScope();
builder.RegisterType<ParametersService>().AsSelf();
builder.Register(c => QSProjectsLib.QSMain.ConnectionStringBuilder).AsSelf();
builder.RegisterType<MySQLProvider>().As<IMySQLProvider>();
#endregion
В библиотеках есть класс CustomProjections добавляющий варианты запросов к базе через QueryOver функциями которых нет в NHibernate но есть в MariaDB.
- Date - Конвертирует поле в дату
- Abs
- Concat
- Concat_WS
- GroupConcat
- Lower
- Upper
Пример использования:
QueryOver.Of<BarcodeOperation>(() => barcodeOperationAlias)
.Select(CustomProjections.Concat(
Projections.Property(() => employeeAlias.LastName),
Projections.Constant(" "),
Projections.Property(() => employeeAlias.FirstName),
Projections.Constant(" "),
Projections.Property(() => employeeAlias.Patronymic)));