Слой приложения 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.