在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 {
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>
///
一个抽象类,提供获取数据库架构的方法。
///
</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>序列:
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<
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;
}
}
六、测试
没有条件的架构查询:
[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,因此还有改进的空间。