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


          在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,因此还有改进的空间。

  • 相关阅读:
    TCP/IP 基础知识
    30 岁的码农人生 ——人生至暗时,你依然能窥见光明
    巨经典论文!推荐系统经典模型Wide & Deep
    带你领略拼多多2020校招笔试题,这样的难度你可以搞定吗?
    做业务、做技术和打杂,你的职场现状是哪种?
    内卷预警,本科生真的很不适合算法岗位吗?
    codeforces 1424J,为了过这题,我把祖传的C++都用上了!
    有了Git这个操作,我再也不怕代码混乱了!
    学会了这一招,距离Git大神不远了!
    好端端的数据结构,为什么叫它SB树呢?
  • 原文地址:https://www.cnblogs.com/faib/p/2464529.html
Copyright © 2020-2023  润新知