WebMatrix数据访问系列目录:
上篇文章简单描述了WebMatrix.Data组件是如何跨数据库工作的。本篇就来揭密WebMatrix.Data内部到底是如何工作的,WebMatrix.Data的源码可以在此处获得,它是与Asp.net MVC 3源码一起发布的,WebMatrix.Data文件组织结构如下:
大约10多个类,下面我们根据接口及其继承类关系逐步分析:
1.IDbProviderFactory,IConnectionConfiguration及其继承类。
IConnectionConfiguration接口中包含两个属性:ConnectionString,ProviderFactory,其中,ProviderFactory的类型为接口IDbProviderFactory,通过属性注入到IConnectionConfiguration中。所以,IConnectionConfiguration的作用只有一个,就是包装Connection的Provider跟ConnectionString。
DbProviderFactoryWrapper对接口IDbProviderFactory中的方法CreateConnection实现需要重点说明下:
public DbConnection CreateConnection(string connectionString) {
if (String.IsNullOrEmpty(_providerName))
{ // If the provider name is null or empty then use the default _providerName = Database.GetDefaultProviderName(); } if (_providerFactory == null) { _providerFactory = DbProviderFactories.GetFactory(_providerName); } DbConnection connection = _providerFactory.CreateConnection(); connection.ConnectionString = connectionString; return connection; }
标记处变量_providerName如果为空,就默认使用Database.GetDefaultProviderName()方法获取数据库提供程序,DataBase类中的相关方法如下:
private const string DefaultDataProviderAppSetting = "
systemData:defaultProvider
"; internal static string GetDefaultProviderName() { string providerName; // Get the default provider name from config if there is any if (!_configurationManager.AppSettings.TryGetValue(DefaultDataProviderAppSetting, out providerName)) { providerName = SqlCeProviderName; } return providerName; }
代码标记位置解释了上篇文章中关于如果在配置文件连接字符串中未提供providerName会发生的种种情况。
2.IConfigurationManager及类ConfigurationManagerWrapper
接口IConfigurationManager的作用其实很简单,就是用来管理在配置文件中的连接字符串信息,该连接字符串信息已经用接口IConnectionConfiguration包装了,其方法签名正式这么做的:
internal interface IConfigurationManager {
IConnectionConfiguration GetConnection(string name);
IDictionary<string, string> AppSettings { get; } }
提供的属性AppSettings的值就是加载配置文件中的AppSettings节的相关键值对 。
ConfigurationManagerWrapper的实现才是最关键的,AppSettings实现如下:
public IDictionary<string, string> AppSettings { get { if (_appSettings == null) { _appSettings = (from string key in ConfigurationManager.AppSettings select key).ToDictionary(key => key, key => ConfigurationManager.AppSettings[key]); } return _appSettings; } }
之前提到,WebMatrix.Data可以用作SSCE(Sql Server Compact Edition)数据库访问组件,就是通过方法GetConnection组装的SSCE连接字符串的:
public IConnectionConfiguration GetConnection(string name) { return GetConnection(name, GetConnectionConfigurationFromConfig, File.Exists); } private static IConnectionConfiguration GetConnectionConfigurationFromConfig(string name) { ConnectionStringSettings setting = ConfigurationManager.ConnectionStrings[name]; if (setting != null) { return new ConnectionConfiguration(setting.ProviderName, setting.ConnectionString); } return null; } internal IConnectionConfiguration GetConnection(string name, Func<string, IConnectionConfiguration> getConfigConnection, Func<string, bool> fileExists) { // First try config IConnectionConfiguration configuraitonConfig = getConfigConnection(name); if (configuraitonConfig != null) { return configuraitonConfig; } // Then try files under the |DataDirectory| with the supported extensions // REVIEW: We sort because we want to process mdf before sdf (we only have 2 entries)
foreach (var handler in _handlers.OrderBy(h => h.Key)) { string fileName = Path.Combine(_dataDirectory, name + handler.Key); if (fileExists(fileName)) { return handler.Value.GetConnectionConfiguration(fileName); } }
return null; }
上述代码中可以看到,连接字符串信息采取配置优先,亦即如果配置文件有相关配置连接字符串信息,那么就返回封装后的连接字符串信息IConnectionConfiguration 。反之,如果未能找到指定的连接字符串信息,那么就开始获取SSCE或者Sql Server Express的连接字符串信息。
获取SSCE/Sql Server Express的连接字符串信息分别用两个实现了IDbFileHandler的类SqlCeDbFileHandler跟SqlServerDbFileHandler实现的:
看一下SqlCeDbFileHandler的方法GetConnectionString:
public IConnectionConfiguration GetConnectionConfiguration(string fileName) { // Get the default provider name string providerName = Database.GetDefaultProviderName(); Debug.Assert(!String.IsNullOrEmpty(providerName), "Provider name should not be null or empty"); string connectionString = GetConnectionString(fileName); return new ConnectionConfiguration(providerName, connectionString); } public static string GetConnectionString(string fileName) { if (Path.IsPathRooted(fileName)) { return String.Format(CultureInfo.InvariantCulture, SqlCeConnectionStringFormat, fileName); } // Use |DataDirectory| if the path isn't rooted
string dataSource = @"|DataDirectory|\" + Path.GetFileName(fileName);
return String.Format(CultureInfo.InvariantCulture, SqlCeConnectionStringFormat, dataSource); }
上篇文章提到如果SSCE的数据文件如果不在程序根目录,需要带上根目录及数据库名字才有效,就是因为连接字符串的Data Souce部分是用路径来指定的。
3.核心类Database
看一段熟悉的访问数据的代码:
var db = Database.Open("mysqldb"); var list = db.Query("SELECT * FROM TestTable"); foreach (var item in list) { Console.WriteLine(item.Name); }
现在完整看一下该代码块是如何工作的:
- 首先通过Database类的静态方法Open传入一个命名的连接字符串,Open方法的返回值为Database的一个实例,那么就必须看下Database的构造函数:
internal Database(Func<DbConnection> connectionFactory) { _connectionFactory = connectionFactory; }
- 实例化Database类必须要传入一个委托Func<DbConnection> connectionFactory,目的就是为了能够创建一个DbConnection实例的委托以便在调用Database实例方法Query方法时创建一个DbConnnect实例。
在实例方法Open内部,它的调用是这样的:
private static readonly IConfigurationManager _configurationManager = new ConfigurationManagerWrapper(_dbFileHandlers); private static readonly IDictionary<string, IDbFileHandler> _dbFileHandlers = new Dictionary<string, IDbFileHandler>(StringComparer.OrdinalIgnoreCase) { { ".sdf", new SqlCeDbFileHandler() }, { ".mdf", new SqlServerDbFileHandler() } }; public static Database Open(string name) { if (String.IsNullOrEmpty(name)) { throw ExceptionHelper.CreateArgumentNullOrEmptyException("name"); } return OpenNamedConnection(name, _configurationManager); } internal static Database OpenNamedConnection(string name, IConfigurationManager configurationManager) { // Opens a connection using the connection string setting with the specified name IConnectionConfiguration configuration = configurationManager.GetConnection(name); if (configuration != null) { // We've found one in the connection string setting in config so use it return OpenConnectionInternal(configuration); } throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, DataResources.ConnectionStringNotFound, name)); } private static Database OpenConnectionInternal(IConnectionConfiguration connectionConfig) { return OpenConnectionStringInternal(connectionConfig.ProviderFactory, connectionConfig.ConnectionString); } internal static Database OpenConnectionStringInternal(IDbProviderFactory providerFactory, string connectionString) { return new Database(() => providerFactory.CreateConnection(connectionString)); }
OpenNamedConnection方法内使用IConfigurationManager获取一个IConnectionConfiguration,委托Func<DbConnection> connectionFactory的获取使用的是IDbProviderFactory。
- 总之,最终的处理的结果是实例化一个DataBase实例,它带有一个能够创建DbConnection的委托;其实,还可以通过类Database的静态方法OpenConnectionString打开一个未命名的连接字符串,方法重载如下:
public static Database OpenConnectionString(string connectionString) public static Database OpenConnectionString(string connectionString, string providerName)
注意:如果使用的不是SSCE/Sql Server Express数据库,那么必须在配置文件中指定ProviderName(在connectionString或AppSettings指定均可,AppSettings中使用key值“systemData:defaultProvider”)。
- 在实例化一个Database后,接下来就是要执行sql语句:
public IEnumerable<dynamic> Query(string commandText, params object[] parameters) { if (String.IsNullOrEmpty(commandText)) { throw ExceptionHelper.CreateArgumentNullOrEmptyException("commandText"); } // Return a readonly collection return QueryInternal(commandText, parameters).ToList().AsReadOnly(); } private IEnumerable<dynamic> QueryInternal(string commandText, params object[] parameters) { EnsureConnectionOpen(); DbCommand command = Connection.CreateCommand(); command.CommandText = commandText; AddParameters(command, parameters); using (command) { IEnumerable<string> columnNames = null; using (DbDataReader reader = command.ExecuteReader()) { foreach (DbDataRecord record in reader) { if (columnNames == null) { columnNames = GetColumnNames(record); } yield return newDynamicRecord(columnNames, record); } } } } private static void AddParameters(DbCommand command, object[] args) { if (args == null) { return; } // Create numbered parameters IEnumerable<DbParameter> parameters = args.Select((o, index) => { var parameter = command.CreateParameter(); parameter.ParameterName =index.ToString(CultureInfo.InvariantCulture); parameter.Value = o; return parameter; }); foreach (var p in parameters) { command.Parameters.Add(p); } }
注意:AddParameters方法可以得知,如果参数存在的情况下,参数名字是以索引0开始,然后顺序排列sql语句参数,类似如下:
INSERT INTO Favorites (Name, Genre, ReleaseYear) VALUES (@0, @1, @2)
查询的结果集中的每条记录为DynamicRecord的动态类型,每次查询时记录的列值动态属性与数据库中的字段名字一 一对应。
4.DynamicRecord
DynamicRecord类重写了DynamicObject中的虚拟方法,实现了接口ICustomTypeDescriptor。看一下其构造函数:
internal DynamicRecord(IEnumerable<string> columnNames, IDataRecord record) { Debug.Assert(record != null, "record should not be null"); Debug.Assert(columnNames != null, "columnNames should not be null"); Columns = columnNames.ToList(); Record = record; }
DynamicRecord类缓存了查询的所有列及每个记录列值IDataRecord,然后通过实现ICustomTypeDescriptor方法GetProperties()获取所有动态属性:
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() { // Get the name and type for each column name var properties = from columnName in Columns let columnIndex = Record.GetOrdinal(columnName) let type = Record.GetFieldType(columnIndex) select new
DynamicPropertyDescriptor
(columnName, type); return new PropertyDescriptorCollection(properties.ToArray(), readOnly: true); }
至于每个记录的列对应的动态属性则由DynamicPropertyDescriptor类描述,它实现了抽象类PropertyDescriptor,DynamicPropertyDescriptor类属性描述的Set,Get方法如下:
public override object GetValue(object component) { DynamicRecord record = component as DynamicRecord; // REVIEW: Should we throw if the wrong object was passed in? if (record != null) { return record[Name]; } return null; } public override void SetValue(object component, object value) { throw new InvalidOperationException( String.Format(CultureInfo.CurrentCulture, DataResources.RecordIsReadOnly, Name)); }
在看下面核心部分,通过继承类DynamicObject并重写其方法TryGetMember到达字段到动态属性的映射
public override bool TryGetMember(GetMemberBinder binder, out object result) { result = this[binder.Name]; return true; }
由于没有重写TrySetMember方法,所以它是一个只读属性,结果集中的每条记录对应的动态属性的值是只读的,无法更改。