Thinking In Design Pattern——Query Object模式


Query Object模式

Query Object:可以在領域服務層構造查詢然后傳給資源庫使用,並使用某種查詢翻譯器將對象查詢(Query)翻譯成底層數據庫持久化框架可以理解的查詢(即翻譯成一條Sql 語句)。而Query Object即可以理解為表示數據庫查詢的對象。且可以構造任意查詢,然后傳給Repository。Query Object模式的主要好處是它完全將底層的數據庫查詢語言抽象出來。

如果沒有某種查詢機制,我們的持久化層可能會這樣定義方法:

    public interface IOrderRepository
    {
        IEnumerable<Order> FindAll(Query query);
        IEnumerable<Order> FindAllVipCustomer();
        IEnumerable<Order> FindOrderBy(Guid customerId);
        IEnumerable<Order> FindAllCustomersWithOutOrderId();
    }

很明顯,可以看出持久化層很不簡潔,Repository將充滿大量檢索方法,而我們希望我們的持久化層盡量簡潔些,根據傳入參數能夠動態的翻譯成數據庫查詢語言,就像下面寫的這樣:

public interface IOrderRepository
    {       
         IEnumerable<Order> FindBy(Query query);
         IEnumerable<Order> FindBy(Query query, int index, int count);         
    }

這個Query就是核心——一個表示數據庫查詢的對象,好處是顯而易見的:完全將底層的數據庫查詢語言抽象出來,因此將數據持久化和檢索的基礎設施關注點從業務層中分離出來。

Query Object模式的架構

  • 添加一個枚舉,CriteriaOperator:
public enum CriteriaOperator
    {
        Equal,//=
        LessThanOrEqual,// <=
        NotApplicable//// TODO: 省略了其他的操作符,可繼續添加
    }
  • 接着添加Criterion類,表示構成查詢的過濾器部分:指定一個實體屬性(OR  Mapping)、要比較的值以及比較方式
 public class Criterion
    {
        private string _propertyName;//實體屬性
        private object _value;//進行比較的值
        private CriteriaOperator _criteriaOperator;//何種比較方式

        public Criterion(string propertyName, object value, CriteriaOperator criteriaOperator)
        {
            _propertyName = propertyName;
            _value = value;
            _criteriaOperator = criteriaOperator;
        }

        public string PropertyName 
        {
            get { return _propertyName; }
        }

        public object Value
        {
            get { return _value; }
        }

        public CriteriaOperator criteriaOperator
        {
            get { return _criteriaOperator; }
        }
        /// <summary>
        /// Lambda表達式樹:創建一個過濾器
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="expression"></param>
        /// <param name="value"></param>
        /// <param name="criteriaOperator"></param>
        /// <returns></returns>
        public static Criterion Create<T>(Expression<Func<T, object>> expression, Object value, CriteriaOperator criteriaOperator)
        {
            string propertyName = PropertyNameHelper.ResolvePropertyName<T>(expression);
            Criterion myCriterion = new Criterion(propertyName, value, criteriaOperator);
            return myCriterion;
        }
    }
  • 為了避免在構建查詢時出現令人畏懼的魔幻字符串,我們創建一個輔助方法,使用表達式參數。
public static class PropertyNameHelper
    {
        
        public static string ResolvePropertyName<T>(Expression<Func<T, object>> expression)
        {
            var expr = expression.Body as MemberExpression;
            if (expr==null)
            {
                var u = expression.Body as UnaryExpression;
                expr = u.Operand as MemberExpression;
            }
            return expr.ToString().Substring(expr.ToString().IndexOf(".")+1);
        }
    }

這樣就可以像查詢中添加一個新的查詢條件:

query.Add(Criterion.Create<Order>(c=>c.CustomerId,customerId,CriteriaOperator.Equal));

而不是使用魔幻字符串:

  query.Add(new Criterion("CustomerId", customerId, CriteriaOperator.Equal));
  • 下面要創建表示查詢的排序屬性:
 public class OrderByClause
    {
        public string PropertyName { get; set; }
        public bool Desc { get; set; }
    }
  • 接着,創建另一個枚舉,確定如何各個Criterion進行評估:
public enum QueryOperator
    {
        And,
        Or            
    }
  • 有時候的復雜非常難以創建,在這些情況下,可以使用指向數據庫視圖或存儲過程的命名查詢,添加一個QueryName來存放查詢列表:
 public enum QueryName
    {       
        Dynamic = 0,//動態創建
        RetrieveOrdersUsingAComplexQuery = 1//使用已經創建好了的存儲過程、視圖、特別是查詢比較復雜時使用存儲過程
    }
  • 最后,添加Query類,將Query Object模式組合在一起:
    public class Query
    {
        private QueryName _name;
        private IList<Criterion> _criteria;

        public Query()
            : this(QueryName.Dynamic, new List<Criterion>())
        { }

        public Query(QueryName name, IList<Criterion> criteria)
        { 
            _name = name;
            _criteria = criteria;
        }

        public QueryName Name
        {
            get { return _name; }
        }
        /// <summary>
        /// 判斷該查詢是否已經動態生成或與Repository中某個預先建立的查詢相關
        /// </summary>
        /// <returns></returns>
        public bool IsNamedQuery()
        {
            return Name != QueryName.Dynamic;
        }

        public IEnumerable<Criterion> Criteria
        {
            get {return _criteria ;}
        }          

        public void Add(Criterion criterion)
        {
            if (!IsNamedQuery())// 動態查詢
                _criteria.Add(criterion);
            else
                throw new ApplicationException("You cannot add additional criteria to named queries");
        }

        public QueryOperator QueryOperator { get; set; }

        public OrderByClause OrderByProperty { get; set; }
    }
  • 最后創建一個工廠類,提供已存在的查詢:
 public static class NamedQueryFactory
    {
        public static Query CreateRetrieveOrdersUsingAComplexQuery(Guid CustomerId)
        {
            IList<Criterion> criteria = new List<Criterion>();
            Query query = new Query(QueryName.RetrieveOrdersUsingAComplexQuery, criteria);

            criteria.Add(new Criterion ("CustomerId", CustomerId, CriteriaOperator.NotApplicable));

            return query;
        }
    }

Query Object在服務層的運用

  • 建立領域模型和領域服務類:
 public class Order
    {
        public Guid Id { get; set; }
        public bool HasShipped { get; set; }
        public DateTime OrderDate { get; set; }
        public Guid CustomerId { get; set; }
    }
  • 添加Repository接口:
  public interface IOrderRepository
    {       
         IEnumerable<Order> FindBy(Query query);
         IEnumerable<Order> FindBy(Query query, int index, int count);         
    }
  • 建立領域服務層:
    public class OrderService
    {
        private IOrderRepository _orderRepository;

        public OrderService(IOrderRepository orderRepository)
        {
            _orderRepository = orderRepository;
        }

        public IEnumerable<Order> FindAllCustomersOrdersBy(Guid customerId)
        {
            IEnumerable<Order> customerOrders = new List<Order>();

            Query query = new Query();
            //推介使用這種
            query.Add(Criterion.Create<Order>(c=>c.CustomerId,customerId,CriteriaOperator.Equal));
            //輸入魔幻字符串,容易出錯
            query.Add(new Criterion("CustomerId", customerId, CriteriaOperator.Equal));
            query.OrderByProperty = new OrderByClause { PropertyName = "CustomerId", Desc = true };

            customerOrders = _orderRepository.FindBy(query); 

            return customerOrders;
        }

        public IEnumerable<Order> FindAllCustomersOrdersWithInOrderDateBy(Guid customerId, DateTime orderDate)
        {
            IEnumerable<Order> customerOrders = new List<Order>();

            Query query = new Query();
            query.Add(new Criterion("CustomerId", customerId, CriteriaOperator.Equal));
            query.QueryOperator = QueryOperator.And; 
            query.Add(new Criterion("OrderDate", orderDate, CriteriaOperator.LessThanOrEqual));
            query.OrderByProperty = new OrderByClause { PropertyName = "OrderDate", Desc = true };

            customerOrders = _orderRepository.FindBy(query);

            return customerOrders;
        }

        public IEnumerable<Order> FindAllCustomersOrdersUsingAComplexQueryWith(Guid customerId)
        {
            IEnumerable<Order> customerOrders = new List<Order>();

            Query query = NamedQueryFactory.CreateRetrieveOrdersUsingAComplexQuery(customerId);

            customerOrders = _orderRepository.FindBy(query);

            return customerOrders;
        }
    }

OrderService類包含3個方法,他們將創建的查詢傳遞給Repository。FindAllCustomersOrdersBy和FindAllCustomersOrdersWithInOrderDateBy方法通過CriterionOrderByClaus添加來創建動態查詢。FindAllCustomersOrdersUsingAComplexQueryWith是命名查詢,使用NamedQueryFactory來創建要傳給Repository的Query Object。

  • 最后創建一個翻譯器:QueryTranslator,將查詢對象翻譯成一條可在數據庫上運行的Sql命令:
public static class OrderQueryTranslator
    {
        private static string baseSelectQuery = "SELECT * FROM Orders ";

        public static void TranslateInto(this Query query, SqlCommand command)
        {
            if (query.IsNamedQuery())
            {
                command.CommandType = CommandType.StoredProcedure;
                command.CommandText = query.Name.ToString();

                foreach (Criterion criterion in query.Criteria)
                {
                    command.Parameters.Add(new SqlParameter("@" + criterion.PropertyName, criterion.Value));
                }
            }
            else
            {
                StringBuilder sqlQuery = new StringBuilder();
                sqlQuery.Append(baseSelectQuery);

                bool _isNotfirstFilterClause = false;

                if (query.Criteria.Count() > 0)
                    sqlQuery.Append("WHERE ");   

                foreach (Criterion criterion in query.Criteria)
                {
                    if (_isNotfirstFilterClause)
                        sqlQuery.Append(GetQueryOperator(query));                                            

                    sqlQuery.Append(AddFilterClauseFrom(criterion));

                    command.Parameters.Add(new SqlParameter("@" + criterion.PropertyName, criterion.Value));

                    _isNotfirstFilterClause = true;
                }

                sqlQuery.Append(GenerateOrderByClauseFrom(query.OrderByProperty));

                command.CommandType = CommandType.Text; 
                command.CommandText = sqlQuery.ToString();
            }
        }

        private static string GenerateOrderByClauseFrom(OrderByClause orderByClause)
        {
            return String.Format("ORDER BY {0} {1}",
                FindTableColumnFor(orderByClause.PropertyName), orderByClause.Desc ? "DESC" : "ASC");          
        }

        private static string GetQueryOperator(Query query)
        {
            if (query.QueryOperator == QueryOperator.And)
                return "AND ";
            else
                return "OR ";
        }

        private static string AddFilterClauseFrom(Criterion criterion)
        {
            return string.Format("{0} {1} @{2} ", FindTableColumnFor(criterion.PropertyName), FindSQLOperatorFor(criterion.criteriaOperator), criterion.PropertyName);
        }

        private static string FindSQLOperatorFor(CriteriaOperator criteriaOperator)
        {
            switch (criteriaOperator)
            { 
                case CriteriaOperator.Equal:
                    return "=";
                case CriteriaOperator.LessThanOrEqual:
                    return "<=";
                default:
                    throw new ApplicationException("No operator defined.");
            }
        }

        private static string FindTableColumnFor(string propertyName)
        {
            switch (propertyName)
            {
                case "CustomerId":
                    return "CustomerId";
                case "OrderDate":
                    return "OrderDate";
                default:
                    throw new ApplicationException("No column defined for this property.");
            }
        }
    }
  • 建立簡單倉儲對象:
 public class OrderRepository : IOrderRepository 
    {        
        private string _connectionString;

        public OrderRepository(string connectionString)
        {
            _connectionString = connectionString;
        }
      
        public IEnumerable<Order> FindBy(Query query)
        {
            // Move to method below with Index and count

            IList<Order> orders = new List<Order>();

            using (SqlConnection connection =
                      new SqlConnection(_connectionString))
            {
                SqlCommand command = connection.CreateCommand();
               
                query.TranslateInto(command);              
                connection.Open();

                using (SqlDataReader reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        orders.Add(new Order
                        {
                            CustomerId = new Guid(reader["CustomerId"].ToString()),
                            OrderDate = DateTime.Parse(reader["OrderDate"].ToString()),
                            Id = new Guid(reader["Id"].ToString())                            
                        });
                    
                     }
                 }                
            }

                return orders;
        }

        public IEnumerable<Order> FindBy(Query query, int index, int count)
        {
            throw new NotImplementedException();            
        }       
    }

測試

 [TestFixture]
    public class SQLQueryTranslatorTests
    {
        [Test]
        public void The_Translator_Should_Produce_Valid_SQL_From_A_Query_Object()
        {
            int customerId = 9;
            string expectedSQL = "SELECT * FROM Orders WHERE CustomerId = @CustomerId ORDER BY CustomerId DESC";

            Query query = new Query();
            query.Add(new Criterion("CustomerId", customerId, CriteriaOperator.Equal));

            //query.Add(Criterion.Create<Order>(c => c.CustomerId, customerId, CriteriaOperator.Equal));
            query.OrderByProperty = new OrderByClause { PropertyName = "CustomerId", Desc = true };

            SqlCommand command = new SqlCommand();
            query.TranslateInto(command);
            Assert.AreEqual(expectedSQL, command.CommandText);
            
        }
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM