posts - 94, comments - 346, trackbacks - 10, articles - 0
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Fireasy.Data系列——数据库架构的整合查询

Posted on 2012-04-22 00:28  faib  阅读(...)  评论(... 编辑 收藏

      在Ado.Net中,DbConnection类的GetSchema方法用于获取数据库提供者的相关架构信息,比如数据类型、表、列等等,然而每种数据库架构的元数据结构都是不一样的。Fireasy.Data提供了一个扩展服务接口,以将四类数据库的架构信息整合在一起,统一定义了最大公有的架构元数据,并在此基础上提供Linq查询的支持。

 

      一、架构元数据的接口

      由于要使用统一的查询,因此需要定义一个标识接口,然后使不同的架构元数据类来实现它。

 

    /// <summary>
    
/// 数据库架构元数据结构。
    
/// </summary>
    public interface ISchemaMetadata
    {
    }

 

      二、架构集合的枚举

      首先定义要支持的架构的类别,基本上每一种数据库都支持以下的这些集合名称。

 

    /// <summary>
    
/// 数据库架构集合的类别。
    
/// </summary>
    public enum SchemaCategory
    {
        /// <summary>
        
/// 所有定义的列的有关信息。
        
/// </summary>
        Columns,
        /// <summary>
        
/// 所支持的数据类型的有关信息。
        
/// </summary>
        DataTypes,
        /// <summary>
        
/// 所有定义的外键的有关信息。
        
/// </summary>
        ForeignKeys,
        /// <summary>
        
/// 作为索引的列的有关信息。
        
/// </summary>
        Indexes,
        /// <summary>
        
/// 所有定义的索引相关列的有关信息。
        
/// </summary>
        IndexColumns,
        /// <summary>
        
/// 所有架构集合的有关信息。 
        
/// </summary>
        MetaDataCollections,
        /// <summary>
        
/// 所有定义的存储过程的有关信息。
        
/// </summary>
        Procedures,
        /// <summary>
        
/// 存储过程中所有参数的有关信息。
        
/// </summary>
        ProcedureParameters,
        /// <summary>
        
/// 数据库公开所保留的关键字的有关信息。
        
/// </summary>
        ReservedWords,
        /// <summary>
        
/// 所支持的限制的有关信息。
        
/// </summary>
        Restrictions,
        /// <summary>
        
/// 所有定义的表的有关信息。
        
/// </summary>
        Tables,
        /// <summary>
        
/// 数据库所定义的用户的有关信息。
        
/// </summary>
        Users,
        /// <summary>
        
/// 所有定义的视图的有关信息。
        
/// </summary>
        Views,
        /// <summary>
        
/// 视图所定义的列的有关信息。
        
/// </summary>
        ViewColumns
    }

 

       三、架构元数据类

       有了以上两个后,就可以定义具体的架构元数据类了,比如Table:

 

    /// <summary>
    
/// 数据库表信息。
    
/// </summary>
    [SchemaCategory(SchemaCategory.Tables)]
    public sealed class Table : ISchemaMetadata
    {
        /// <summary>
        
/// 获取分录名称。
        
/// </summary>
        [SchemaQueryableAttribute(0, ProviderType.MsSql)]
        [SchemaQueryableAttribute(0, ProviderType.SQLite)]
        [SchemaQueryableAttribute(0, ProviderType.MySql)]
        public string TableCatalog { getinternal set; }

        /// <summary>
        
/// 获取架构名称。
        
/// </summary>
        [SchemaQueryableAttribute(1, ProviderType.MsSql)]
        [SchemaQueryableAttribute(0, ProviderType.Oracle)]
        [SchemaQueryableAttribute(1, ProviderType.MySql)]
        public string TableSchema { getinternal set; }

        /// <summary>
        
/// 获取表名称。
        
/// </summary>
        [SchemaQueryableAttribute(2, ProviderType.MsSql)]
        [SchemaQueryableAttribute(1, ProviderType.Oracle)]
        [SchemaQueryableAttribute(2, ProviderType.SQLite)]
        [SchemaQueryableAttribute(2, ProviderType.MySql)]
        public string TableName { getinternal set; }

        /// <summary>
        
/// 获取表类型。
        
/// </summary>
        [SchemaQueryableAttribute(3, ProviderType.MsSql)]
        [SchemaQueryableAttribute(3, ProviderType.SQLite)]
        [SchemaQueryableAttribute(3, ProviderType.MySql)]
        public string TableType { getinternal set; }

        /// <summary>
        
/// 获取表的描述。
        
/// </summary>
        public string Description { getinternal set; }
    }

        在以上的代码中,分别用到了两个特性,SchemaCategoryAttribute标识了该类所属的架构类别,使用特性的目的,在于避免使用字符串,这个将在后面介绍。

        另一个特性SchemaQueryableAttribute特性则是定义了元数据属性在查询限制数组中的索引位置,因为每一种数据库类型对于同一个属性所限制的位置是不同的,因此需要为每一种数据库类别定义一个特性。

 

        四、架构扩展服务类

        首先定义一个抽象类,对底层的处理进行封装,然后开放每一类架构的信息获取方法出来,不同的数据库类型再进行重写,以使信息之间一一对应。

 

    /// <summary>
    
/// 一个抽象类,提供获取数据库架构的方法。
    
/// </summary>
    public abstract class BaseSchema : ISchemaProvider
    {
        /// <summary>
        
/// 获取或设置提供者服务的上下文。
        
/// </summary>
        public ServiceContext ServiceContext { getset; }

        /// <summary>
        
/// 获取指定类型的数据库架构信息。
        
/// </summary>
        
/// <typeparam name="T">架构信息的类型。</typeparam>
        
/// <param name="predicate">用于测试架构信息是否满足条件的函数。</param>
        
/// <returns></returns>
        public virtual IEnumerable<T> GetSchemas<T>(Expression<Func<T, bool>> predicate = nullwhere T : ISchemaMetadata
        {
            var category = GetSchemaCategory<T>();
            var restrictionValues = SchemaQueryTranslator.GetRestriction(ServiceContext.Database.Provider.ProviderType, typeof(T), predicate);
            DataTable table;

            using (var connection = ServiceContext.Database.CreateConnection())
            {
                var collectionName = GetSchemaCategoryName(category);
                try
                {
                    connection.TryOpen();
                    table = connection.GetSchema(collectionName, InitRestrictionValues(connection, category, restrictionValues));
                }
                catch (Exception ex)
                {
                    throw new SchemaNotSupportedtException(collectionName, ex);
                }
                finally
                {
                    connection.TryClose();
                }
            }
            return ReturnSchemaElements<T>(category, table);
        }

        /// <summary>
        
/// 获取指定类型的数据库架构信息。
        
/// </summary>
        
/// <param name="collectionName">架构信息类别名称。</param>
        
/// <param name="restrictionValues">列限制数组。</param>
        
/// <returns></returns>
        public virtual DataTable GetSchema(string collectionName, string[] restrictionValues)
        {
            DataTable table;
            using (var connection = ServiceContext.Database.CreateConnection())
            {
                connection.TryOpen();
                table = connection.GetSchema(collectionName, restrictionValues);
                connection.TryClose();
            }
            return table;
        }
    }

        在以上的代码中,第一步:使用GetSchemaCategoryName方法获得Ado.Net中所支持集合名称,如Tables、Columns。

 

        /// 获取架构的名称。
        
/// </summary>
        
/// <param name="category">架构信息类别。</param>
        
/// <returns></returns>
        protected virtual string GetSchemaCategoryName(SchemaCategory category)
        {
            return category.ToString();
        }

 

         如果集合名称不是使用枚举的名称,则在具体的子类中重写这个方法指定就可以了。

 

         第二步,使用SchemaQueryTranslator类对传入的Linq查询表达式进行解析,得到原生的restrictionValues,这个数组作为connection.GetSchema方法的第二个参数传入。

 

         第三步,对查询得到的DataTable进行解析,返回我们需要的IEnumerable<T>序列:

 

        private IEnumerable<T> ReturnSchemaElements<T>(SchemaCategory category, DataTable table)
        {
            IEnumerable @enum = null;
            switch (category)
            {
                case SchemaCategory.Columns:
                    @enum = GetColumns(table, null);
                    break;
                case SchemaCategory.DataTypes:
                    @enum = GetDataTypes(table, null);
                    break;
                case SchemaCategory.ForeignKeys:
                    @enum = GetForeignKeys(table, null);
                    break;
                case SchemaCategory.IndexColumns:
                    @enum = GetIndexColumns(table, null);
                    break;
                case SchemaCategory.Indexes:
                    @enum = GetIndexs(table, null);
                    break;
                case SchemaCategory.MetaDataCollections:
                    @enum = GetMetaDataCollections(table, null);
                    break;
                case SchemaCategory.ProcedureParameters:
                    @enum = GetProcedureParameters(table, null);
                    break;
                case SchemaCategory.Procedures:
                    @enum = GetProcedures(table, null);
                    break;
                case SchemaCategory.ReservedWords:
                    @enum = GetReservedWords(table, null);
                    break;
                case SchemaCategory.Restrictions:
                    @enum = GetRestrictions(table, null);
                    break;
                case SchemaCategory.Tables:
                    @enum = GetTables(table, null);
                    break;
                case SchemaCategory.Users:
                    @enum = GetUsers(table, null);
                    break;
                case SchemaCategory.ViewColumns:
                    @enum = GetViewColumns(table, null);
                    break;
                case SchemaCategory.Views:
                    @enum = GetViews(table, null);
                    break;
            }

            if (@enum != null)
            {
                foreach (var item in @enum)
                {
                    yield return (T)item;
                }
            }
        }

        每一个初始架构信息的方法都定义成了虚方法了,因此在子类中还可以进行信息的转换,就象在OracleSchema中,我们可以对Table的信息进行丰富,增加了获取表描述信息的提取:

 

        /// <summary>
        
/// 获取 <see cref="Table"/> 元数据序列。
        
/// </summary>
        
/// <param name="table">架构信息的表。</param>
        
/// <param name="action">用于填充元数据的方法。</param>
        
/// <returns></returns>
        protected override IEnumerable<Table> GetTables(DataTable table, Action<Table, DataRow> action)
        {
            foreach (DataRow row in table.Rows)
            {
                var item = new Table
                    {
                        TableSchema = row["OWNER"].ToString(),
                        TableName = row["TABLE_NAME"].ToString(),
                        TableType = row["TYPE"].ToString()
                    };
                item.Description = OracleSchemaHelper.GetTableDescription(ServiceContext.Database, item.TableSchema, item.TableSchema);

                if (action != null)
                {
                    action(item, row);
                }
                yield return item;
            }
        }

 

        五、架构查询的表达式解析类

        其实本篇的重点在于此类,它对传入查询的表达式进行解析,并返回一个限制数组,如果你对表达式有所了解,相信一看就明白其中的原理了。

 

    internal sealed class SchemaQueryTranslator : Common.Linq.Expressions.ExpressionVisitor
    {
        private Dictionary<intstring> m_dic;
        private int m_index = -1;
        private int m_maxIndex;
        private readonly Type m_metadataType;
        private readonly ProviderType m_providerType;

        public SchemaQueryTranslator(ProviderType providerType, Type metadataType)
        {
            m_providerType = providerType;
            m_metadataType = metadataType;
            InitDictionary();
        }

        /// <summary>
        
/// 对表达式进行解析,并返回限制数组。
        
/// </summary>
        
/// <param name="providerType">数据提供者类别。</param>
        
/// <param name="metadataType">架构元数组类型。</param>
        
/// <param name="expression">查询表达式。</param>
        
/// <returns></returns>
        public static string[] GetRestriction(ProviderType providerType, Type metadataType, Expression expression)
        {
            var translator = new SchemaQueryTranslator(providerType, metadataType);
            return translator.GetRestrictionValues(expression);
        }

        private string[] GetRestrictionValues(Expression expression)
        {
            if (expression != null)
            {
                Visit(expression);
            }
            return TrimEmptyArray();
        }

        /// <summary>
        
/// 初始化字典,找出架构元数据类中定义了 <see cref="SchemaQueryableAttribute"/> 特性的所有属性。
        
/// </summary>
        private void InitDictionary()
        {
            m_dic = new Dictionary<intstring>();
            var properties = m_metadataType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            foreach (var property in properties)
            {
                var attribute = property.GetCustomAttributes<SchemaQueryableAttribute>().FirstOrDefault(s => s.ProviderType == m_providerType);
                if (attribute == null)
                {
                    continue;
                }
                //使用索引作为键值
                m_dic.Add(attribute.Index, null);
            }
        }

        /// <summary>
        
/// 访问表达式树。
        
/// </summary>
        
/// <param name="expression"></param>
        
/// <returns></returns>
        protected override Expression Visit(Expression expression)
        {
            switch (expression.NodeType)
            {
                case ExpressionType.MemberAccess:
                    return VisitMember((MemberExpression)expression);
                case ExpressionType.Equal:
                    return VisitBinary((BinaryExpression)expression);
                case ExpressionType.Constant:
                    return VisitConstant((ConstantExpression)expression);
            }
            return base.Visit(expression);
        }

        /// <summary>
        
/// 访问二元运算表达式。
        
/// </summary>
        
/// <param name="binaryExp"></param>
        
/// <returns></returns>
        protected override Expression VisitBinary(BinaryExpression binaryExp)
        {
            //属性在运算符的右边
            var memberExp = binaryExp.Right as MemberExpression;
            if (memberExp != null &&
                memberExp.Member.DeclaringType == m_metadataType)
            {
                Visit(binaryExp.Right);
                Visit(binaryExp.Left);
            }
            else
            {
                Visit(binaryExp.Left);
                Visit(binaryExp.Right);
            }
            //复位
            m_index = -1;
            return binaryExp;
        }

        protected override Expression VisitMember(MemberExpression memberExp)
        {
            //如果属性是架构元数据类的成员
            if (memberExp.Member.DeclaringType == m_metadataType)
            {
                var attribute = memberExp.Member.GetCustomAttributes<SchemaQueryableAttribute>().FirstOrDefault(s => s.ProviderType == m_providerType);
                if (attribute == null)
                {
                    throw new SchemaQueryNotSupportedException(memberExp.Member.Name);
                }
                //记录下当前的索引,以及目前的最大索引
                m_index = attribute.Index;
                m_maxIndex = Math.Max(m_maxIndex, m_index + 1);
                return memberExp;
            }
            else
            {
                //值或引用
                var exp = (Expression)memberExp;
                if (memberExp.Type.IsValueType)
                {
                    exp = Expression.Convert(memberExp, typeof(object));
                }
                var lambda = Expression.Lambda<Func<object>>(exp);
                var fn = lambda.Compile();
                //转换为常量表达式
                return Visit(Expression.Constant(fn(), memberExp.Type));
            }
        }

        protected override Expression VisitConstant(ConstantExpression constExp)
        {
            if (m_index == -1)
            {
                return constExp;
            }
            //没有复位的情况下,记录值
            m_dic[m_index] = constExp.Value.ToString();
            return constExp;
        }

        /// <summary>
        
/// 删除空的数据元素
        
/// </summary>
        
/// <returns></returns>
        private string[] TrimEmptyArray()
        {
            //最大范围
            var array = new string[m_maxIndex];
            for (var i = 0; i < m_maxIndex; i++)
            {
                if (m_dic.ContainsKey(i))
                {
                    array[i] = m_dic[i];
                }
            }
            return array;
        }
    }

 

        六、测试

        没有条件的架构查询:

        [Test]
        public void GetTables()
        {
            Console.WriteLine(TimeWatcher.Watch(() =>
                InvokeTest(database =>
                    {
                        var schema = database.Provider.GetService<ISchemaProvider>();
                        foreach (var table in schema.GetSchemas<Table>())
                        {
                            PrintSchema(table);
                        }
                        Console.WriteLine();
                    })));
        }

        使用表达式的架构查询:

 

        [Test]
        public void GetTablesQuery()
        {
            Console.WriteLine(TimeWatcher.Watch(() =>
                InvokeTest(database =>
                    {
                        var schema = database.Provider.GetService<ISchemaProvider>();
                        foreach (var table in schema.GetSchemas<Table>(s => s.TableName == "products"))
                        {
                            PrintSchema(table);
                        }
                        Console.WriteLine();
                    })));
        }

 

         当然,虽然在一定程度上解决了架构查询的问题,但是仍然在于一些缺陷,主要表达在数据库之间一些微妙的差别,比如oracle的大小写敏感问题,以及它是使用owner,而sqlserver使用schema,因此还有改进的空间。