Слой приложения persistence layer является в определённом смысле уникальным в смысле узкой направленности его функционала по сравнению с другими слоями приложения. Если рассматривать его только для работы с реляционными базами данных, то реализацию функционала слоя можно разбить на два основных варианта - с использованием ORM фреймворка и без использования ORM фреймворка. Каждый из этих вариантов можно реализовать достаточно универсальным образом.
Реализация с использованием ORM фреймворка прекрасно описана в разделах 18.1 и 18.2 в книге Бауэр К., Кинг Г., Грегори Г. Java Persistence API и Hibernate. ДМК Пресс, 2017.
В этой статье рассмотрен пример реализации слоя persistence layer без использования ORM фреймворка. Предлагаемое решение является простым и в тоже время достаточно универсальным для использования в языках программирования, поддерживающих объектную модель.
Структуру слоя persistence layer рассмотрим в виде трёхуровневой иерархии функционала.
Эти уровни иерархии можно рассматривать как подслои persistence layer.
Фасад слоя - набор объектов доступа к внешним персистентным данным (DAO объектов). Через фасад происходит доступ к функционалу слоя из вышележащих слоёв приложения. Фасад скрывает детали реализации работы с базой данных от вышележащих слоёв приложения.
Механизмы обработки персистентных данных.
Механизмы доступа к реляционным базам данных.
Модель данных слоя persistence layer в данном примере представлена классом Factor. Его структура данных соответствует структуре данных в строке таблицы tblFactors в базе данных.
public class Factor
{
public int Id;
public string Name;
public decimal Value;
}
Рассмотрим примеры кода на C#, который реализует функционал слоя.
Объекты доступа к внешним персистентным данным (DAO объекты) являются наследниками базового класса ABaseDAO.
/// <summary>базовый класс DAO объектов</summary>
public abstract class ABaseDAO
{
protected IPersistenceManager persistenceManager;
}
/// <summary>имплиментация DAO объекта для работы с сущностью Factor</summary>
public class FactorDAO : ABaseDAO
{
/// <summary>
/// в конструктор через параметр инжектируется объект типа SqlPersistenceManager
/// </summary>
public FactorDAO(IPersistenceManager persistenceManager)
{
this.persistenceManager = persistenceManager;
}
/// <summary>
/// вставка новой строки в таблицу tblFactors
/// </summary>
public void Insert(Factor entity)
{
var sqlQuery = "INSERT INTO tblFactors(Id,Name,Value) VALUES(@Id,@Name,@Value) ";
var listDbParameters = new List<DbParameter> { persistenceManager.CreateQueryParameter("@Id", entity.Id) };
listDbParameters.Add(persistenceManager.CreateQueryParameter("@Name", entity.Name));
listDbParameters.Add(persistenceManager.CreateQueryParameter("@Value", entity.Value));
persistenceManager.PersistData(sqlQuery, listDbParameters.ToArray());
}
/// <summary>
/// обновление данных строки в таблице tblFactors
/// </summary>
public void Update(Factor entity)
{
var sqlQuery = "UPDATE tblFactors SET Name=@Name, Value=@Value WHERE Id=@Id ";
var listDbParameters = new List<DbParameter> { persistenceManager.CreateQueryParameter("@Id", entity.Id) };
listDbParameters.Add(persistenceManager.CreateQueryParameter("@Name", entity.Name));
listDbParameters.Add(persistenceManager.CreateQueryParameter("@Value", entity.Value));
persistenceManager.PersistData(sqlQuery, listDbParameters.ToArray());
}
/// <summary>
/// удаление строки из таблицы tblFactors
/// </summary>
public void Delete(Factor entity)
{
var sqlQuery = "DELETE FROM tblFactors WHERE Id=@Id ";
var listDbParameters = new List<DbParameter> { persistenceManager.CreateQueryParameter("@Id", entity.Id) };
persistenceManager.PersistData(sqlQuery, listDbParameters.ToArray());
}
/// <summary>
/// извление строки данных из таблицы tblFactors по первичному ключу Id таблицы
/// </summary>
public List<Factor> SelectById(int id)
{
var sqlQuery = "SELECT Id, Name, Value FROM tblFactors WHERE Id=@Id ";
var listDbParameters = new List<DbParameter> { persistenceManager.CreateQueryParameter("@Id", id) };
DbParameter[] dbParameters = listDbParameters.ToArray();
DataTable dataTable = persistenceManager.RetrieveData(sqlQuery, dbParameters);
List<Factor> list = convertDataTableToEntityList(dataTable);
return list;
}
/// <summary>
/// В этом методе данные из объекта типа DataTable конвертируются в коллекцию объектов типа Factor
/// </summary>
protected List<Factor> convertDataTableToEntityList(DataTable dataTable)
{
// ........................
}
}
2. Объекты, используемые в механизме обработки персистентных данных, реализуют интерфейс IPersistenceManager.
/// <summary>базовый интерфейс механизма обработки персистентных данных</summary>
public interface IPersistenceManager
{
DataTable RetrieveData(string strQuery, DbParameter[] sqlQueryParams);
DataTable RetrieveData(string strQuery);
void PersistData(string strQuery, DbParameter[] sqlQueryParams);
void PersistData(string strQuery);
DbParameter CreateQueryParameter(string parameterName, object value);
}
/// <summary>абстрактный базовый класс механизма обработки персистентных данных</summary>
public abstract class APersistenceManager : IPersistenceManager
{
/// <summary>Объект для получения соединения с бд</summary>
protected IDbConnectionManager connManager;
#region запросы на извлечение данных из БД
public DataTable RetrieveData(string strQuery, DbParameter[] sqlQueryParams)
{
DataTable dataTable = new DataTable();
DbCommand command = CreateCommand(strQuery, connManager.GetConnection());
AddQueryParameters(command, sqlQueryParams);
DbDataAdapter adapter = CreateDataAdapter(command);
adapter.Fill(dataTable);
return dataTable;
}
public DataTable RetrieveData(string strQuery)
{
return RetrieveData(strQuery, null);
}
#endregion
#region запросы на изменение данных в БД
public void PersistData(string strQuery, DbParameter[] sqlQueryParams)
{
DbCommand command = CreateCommand(strQuery, connManager.GetConnection());
AddQueryParameters(command, sqlQueryParams);
command.ExecuteNonQuery();
}
public void PersistData(string strQuery)
{
PersistData(strQuery, null);
}
#endregion
#region методы, функционал которых необходимо переопределить в зависимости от используемого типа базы данных
protected abstract DbCommand CreateCommand(string strQuery, DbConnection conn);
protected abstract DbDataAdapter CreateDataAdapter(DbCommand command);
public abstract DbParameter CreateQueryParameter(string parameterName, object value);
#endregion
/// <summary>
/// присоединяет коллекцию параметров, используемых в запросе к базе данных, к объекту DbCommand
/// </summary>
protected void AddQueryParameters(DbCommand command, DbParameter[] queryParams)
{
if (queryParams != null)
{
foreach (DbParameter param in queryParams)
{
command.Parameters.Add(param);
}
}
}
}
Если в приложении используется несколько типов баз данных, то для каждого типа должен быть реализована пара объектов - PersistenceManager + ConnectionManager.
Для работы с базами данных Microsoft Sql server - это объекты типа SqlPersistenceManager и SqlConnectionManager.
Для работы с базами данных Oracle - это объекты типа OraclePersistenceManager и OracleConnectionManager.
/// <summary>
/// имплиментация функционала механизма обработки персистентных данных для бд ms sql server
/// </summary>
public class SqlPersistenceManager : APersistenceManager
{
/// <summary>
/// в конструктор через параметр инжектируется объект типа SqlConnectionManager
/// </summary>
public SqlPersistenceManager(ISqlConnectionManager connMgr)
{
this.connManager = connMgr;
}
#region override members
protected override DbCommand CreateCommand(string strQuery, DbConnection conn)
{
DbCommand cmd = new SqlCommand(strQuery, (SqlConnection)conn);
return cmd;
}
protected override DbDataAdapter CreateDataAdapter(DbCommand command)
{
return new SqlDataAdapter((SqlCommand)command);
}
/// <summary>Метод для создания параметра запроса</summary>
public override DbParameter CreateQueryParameter(string parameterName, object value)
{
return new SqlParameter(parameterName, value);
}
#endregion
}
public class OraclePersistenceManager : APersistenceManager
{
/// <summary>
/// в конструктор через параметр инжектируется объект типа OracleConnectionManager
/// </summary>
public OraclePersistenceManager(IOracleConnectionManager connMgr)
{
this.connManager = connMgr;
}
#region override members
protected override DbCommand CreateCommand(string strQuery, DbConnection conn)
{
DbCommand cmd = new OracleCommand(strQuery, (OracleConnection)conn);
return cmd;
}
protected override DbDataAdapter CreateDataAdapter(DbCommand command)
{
return new OracleDataAdapter((OracleCommand)command);
}
/// <summary>Метод для создания параметра запроса</summary>
public override DbParameter CreateQueryParameter(string parameterName, object value)
{
return new OracleParameter(parameterName, value);
}
#endregion
}
3. Объекты, используемые в механизме доступа к реляционным базам данных, реализуют интерфейс IDbConnectionManager.
/// <summary>
/// базовый интерфейс механизма доступа к реляционным базам данных
/// </summary>
public interface IDbConnectionManager
{
DbConnection GetConnection();
}
/// <summary>
/// базовый класс, реализующий функционал механизма доступа к реляционным базам данных
/// </summary>
public abstract class ADbConnectionManager : IDbConnectionManager
{
#region поля и свойства класса
/// <summary>Объект соединения с базой данных</summary>
protected DbConnection dbConnection = null;
/// <summary>Строка соединения с базой данных</summary>
protected abstract string connectionString { get; }
#endregion
/// <summary>
/// Возвращает объект соединения с базой данных.
/// </summary>
public DbConnection GetConnection()
{
if (dbConnection == null || dbConnection.State != ConnectionState.Open)
{
createConnection();
}
return dbConnection;
}
/// <summary>
/// Создаёт объект соединения с базой данных
/// </summary>
protected abstract void createConnection();
}
public interface ISqlConnectionManager : IDbConnectionManager
{
}
/// <summary>
/// класс, реализующий функционал механизма доступа к базе данных ms sql server
/// </summary>
public class SqlConnectionManager : ADbConnectionManager, ISqlConnectionManager
{
protected override string connectionString
{
get
{
return AppConfigSettings.SqlConnectionString;
}
}
/// <summary>
/// Создаёт объект соединения с базой данных
/// </summary>
protected override void createConnection()
{
dbConnection = new SqlConnection(connectionString);
dbConnection.Open();
}
}
public interface IOracleConnectionManager : IDbConnectionManager
{
}
public class OracleConnectionManager : ADbConnectionManager, IOracleConnectionManager
{
protected override string connectionString
{
get
{
return AppConfigSettings.ConnectionString;
}
}
/// <summary>
/// Создаёт объект соединения с базой данных
/// </summary>
protected override void createConnection()
{
dbConnection = new OracleConnection(connectionString);
dbConnection.Open();
}
}
Рассмотрим случай, когда приложение работает с несколькими базами данных ms sql server:
history - база данных телеметрии;
ius - база нормативно-справочных данных.
Для соединения с каждой из этих баз данных необходимо добавить в приложение класс, который создаёт объект соединения с ней. Этот класс инжектируется в конструктор класса SqlPersistenceManager при помощи Inversion of control фреймворка.
/// <summary>
/// перегруженный класс, реализующий функционал для соединения с базой данных history
/// </summary>
public class HistorySqlConnectionManager : SqlConnectionManager
{
protected override string connectionString
{
get
{
return AppConfigSettings.HistoryConnectionString;
}
}
}
/// <summary>
/// перегруженный класс, реализующий функционал для соединения с базой данных ius
/// </summary>
public class IusSqlConnectionManager : SqlConnectionManager
{
protected override string connectionString
{
get
{
return AppConfigSettings.IusConnectionString;
}
}
}
При работе с объектами ConnectionManager может возникнуть следующая проблема.
В одном use case приложение может использовать несколько DAO объектов. Предположим, что в use case идёт работа только с одной базой данных. В соответствии с приведенным выше кодом, каждый DAO объект откроет своё соединения с базой данных. Такая ситуация неприемлема и необходимо, чтобы в рамках use case работа с базой данных шла через одно соединение. Этого можно добиться использованием в приложении Inversion of control фреймворка. С его помощью надо задать параметр времени жизни lifetime для объекта (наследника ADbConnectionManager) соединения с базой данных для веб-приложений как per-request, а для standalone-приложений как singleton.
Комментарии (11)
Kerman
17.07.2025 11:29Так это же ORM. Да, не полный, да самописный, но это уже ORM.
AlexViolin Автор
17.07.2025 11:29Есть маппинг из объекта DataTable в список объектов-сущностей. Но это никак нельзя назвать ORM фреймворком.
Kerman
17.07.2025 11:29А Object-Relational Mapper назвать можно?
AlexViolin Автор
17.07.2025 11:29Реализован маппер между объектами DataTable и объектами типа Factor
Kerman
17.07.2025 11:29То есть между реляционными рядами RDBMS и объектами языка с ООП?
AlexViolin Автор
17.07.2025 11:29Фактически происходит маппинг данных из строки таблицы с данными (таблица это объект типа DataTable) на объект типа Factor. Алгоритм маппинга это передача данных по цепочке: таблица с данными в базе данных -> объект DataTable -> коллекция объектов типа Factor
Kerman
17.07.2025 11:29Да я понимаю. Сам же ещё в 2008м году что-то подобное делал и упорно не хотел называть это ORM. Там сильно проще было, в угоду времени, но с кодогенерацией по структуре БД.
А так у вас уже есть маппер, можно прикрутить кодогенерацию по структуре (или модели) и немного расширить возможность составления запросов. Если linq прикрутить, так вообще будет весь EF...
Странно, конечно, что тут вообще DataTable делает. Можно же сразу из ридера грузить без посредников.
AleksejMsk
17.07.2025 11:29У меня есть свой велосипед без ORM - там и доменные объекты и unitofwork и много чего
И да есть работа с бд без orm на чистом ado.net - правда не руками писано а кодогенерация + генерированные тесты. Поддержаны ms sql server и postgresql
https://github.com/KlestovAlexej/Wattle.DemoServer
А это подробная демка библиотеки
granit1986
Что будете делать, если вам надо будет по бизнес-логике вставлять/обновлять больше 1 таблицы за раз?
AlexViolin Автор
Use case будет работать с несколькими DAO объектами. Каждый DAO объект работает со своей таблицей в бд.
granit1986
Ага, а потом один из запросов не пройдёт. И что будет?