Search
Write a publication
Pull to refresh

Пример реализации слоя приложения persistence layer без использования ORM фреймворка

Level of difficultyMedium
Reading time8 min
Views2.3K

Слой приложения persistence layer является в определённом смысле уникальным в смысле узкой направленности его функционала по сравнению с другими слоями приложения. Если рассматривать его только для работы с реляционными базами данных, то реализацию функционала слоя можно разбить на два основных варианта - с использованием ORM фреймворка и без использования ORM фреймворка. Каждый из этих вариантов можно реализовать достаточно универсальным образом.

Реализация с использованием ORM фреймворка прекрасно описана в разделах 18.1 и 18.2 в книге Бауэр К., Кинг Г., Грегори Г. Java Persistence API и Hibernate. ДМК Пресс, 2017.

В этой статье рассмотрен пример реализации слоя persistence layer без использования ORM фреймворка. Предлагаемое решение является простым и в тоже время достаточно универсальным для использования в языках программирования, поддерживающих объектную модель.

Структуру слоя persistence layer рассмотрим в виде трёхуровневой иерархии функционала.
Эти уровни иерархии можно рассматривать как подслои persistence layer.

  1. Фасад слоя - набор объектов доступа к внешним персистентным данным (DAO объектов). Через фасад происходит доступ к функционалу слоя из вышележащих слоёв приложения. Фасад скрывает детали реализации работы с базой данных от вышележащих слоёв приложения.

  2. Механизмы обработки персистентных данных.

  3. Механизмы доступа к реляционным базам данных.

Модель данных слоя persistence layer в данном примере представлена классом Factor. Его структура данных соответствует структуре данных в строке таблицы tblFactors в базе данных.

    public class Factor
    {
        public int Id;
        public string Name;
        public decimal Value;
    }

Рассмотрим примеры кода на C#, который реализует функционал слоя.

  1. Объекты доступа к внешним персистентным данным (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:

  1. history - база данных телеметрии;

  2. 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.

Tags:
Hubs:
-3
Comments11

Articles