在Ado.Net中,DbConnection类的GetSchema方法用于获取数据库提供者的相关架构信息,比如数据类型、表、列等等,然而每种数据库架构的元数据结构都是不一样的。Fireasy.Data提供了一个扩展服务接口,以将四类数据库的架构信息整合在一起,统一定义了最大公有的架构元数据,并在此基础上提供Linq查询的支持。
一、架构元数据的接口
由于要使用统一的查询,因此需要定义一个标识接口,然后使不同的架构元数据类来实现它。
/// 数据库架构元数据结构。
/// </summary>
public interface ISchemaMetadata
{
}
二、架构集合的枚举
首先定义要支持的架构的类别,基本上每一种数据库都支持以下的这些集合名称。
/// 数据库架构集合的类别。
/// </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>
[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 { get; internal set; }
/// <summary>
/// 获取架构名称。
/// </summary>
[SchemaQueryableAttribute(1, ProviderType.MsSql)]
[SchemaQueryableAttribute(0, ProviderType.Oracle)]
[SchemaQueryableAttribute(1, ProviderType.MySql)]
public string TableSchema { get; internal set; }
/// <summary>
/// 获取表名称。
/// </summary>
[SchemaQueryableAttribute(2, ProviderType.MsSql)]
[SchemaQueryableAttribute(1, ProviderType.Oracle)]
[SchemaQueryableAttribute(2, ProviderType.SQLite)]
[SchemaQueryableAttribute(2, ProviderType.MySql)]
public string TableName { get; internal set; }
/// <summary>
/// 获取表类型。
/// </summary>
[SchemaQueryableAttribute(3, ProviderType.MsSql)]
[SchemaQueryableAttribute(3, ProviderType.SQLite)]
[SchemaQueryableAttribute(3, ProviderType.MySql)]
public string TableType { get; internal set; }
/// <summary>
/// 获取表的描述。
/// </summary>
public string Description { get; internal set; }
}
在以上的代码中,分别用到了两个特性,SchemaCategoryAttribute标识了该类所属的架构类别,使用特性的目的,在于避免使用字符串,这个将在后面介绍。
另一个特性SchemaQueryableAttribute特性则是定义了元数据属性在查询限制数组中的索引位置,因为每一种数据库类型对于同一个属性所限制的位置是不同的,因此需要为每一种数据库类别定义一个特性。
四、架构扩展服务类
首先定义一个抽象类,对底层的处理进行封装,然后开放每一类架构的信息获取方法出来,不同的数据库类型再进行重写,以使信息之间一一对应。
/// 一个抽象类,提供获取数据库架构的方法。
/// </summary>
public abstract class BaseSchema : ISchemaProvider
{
/// <summary>
/// 获取或设置提供者服务的上下文。
/// </summary>
public ServiceContext ServiceContext { get; set; }
/// <summary>
/// 获取指定类型的数据库架构信息。
/// </summary>
/// <typeparam name="T">架构信息的类型。</typeparam>
/// <param name="predicate">用于测试架构信息是否满足条件的函数。</param>
/// <returns></returns>
public virtual IEnumerable<T> GetSchemas<T>(Expression<Func<T, bool>> predicate = null) where 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>序列:
{
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的信息进行丰富,增加了获取表描述信息的提取:
/// 获取 <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;
}
}
五、架构查询的表达式解析类
其实本篇的重点在于此类,它对传入查询的表达式进行解析,并返回一个限制数组,如果你对表达式有所了解,相信一看就明白其中的原理了。
{
private Dictionary<int, string> 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<int, string>();
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;
}
}
六、测试
没有条件的架构查询:
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();
})));
}
使用表达式的架构查询:
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,因此还有改进的空间。