ServiceStack.OrmLite中的一些"陷阱"(2)
注:此系列不是说ServiceStack.OrmLite的多个陷阱,这仅仅个人认为是某一个陷阱(毕竟我踩坑了)而引发的思考。
前文说到了项目需要使用两种不同的数据库语言,虽说前文问题已基本解决了,但是我发现OrmLite在设计上有需要改进的地方。正如前面提到的OrmLite为了开发的便捷性,ORM所需要生成SQL语句DialectProvider设置为静态属性(尽管使用了线程安全),但是这样的话DialectProvider便与线程上下文产生耦合。
而一个更优的方法,则是使用代理。第一次对代理产生深刻印象的,便是Java的连接池。[故事准备开始]
[故事中断]说到连接池,试想OrmLite在这种与线程耦合的情况下能否实现?答案是:看实际情况(废话)。
可以的前提是不使用线程池。线程池与连接池要区分清楚,虽然两者的目的都是一样的——实现对象的重用。如果使用了线程池,当线程被重用时,OrmLiteConfig.TSDialectProvider可能被之前的IDbConnection所“沾污”,除非IDbConnection关闭前自动清除该线程上下文的OrmLiteConfig.TSDialectProvider,但是没有代理的话,这明显是不可能的。[中断恢复]
[故事开始]那时候才刚学动态代理不久(Java 动态代理机制分析及扩展),在学到数据库开发连接池的概念时,起初没觉得有什么特别。突然一天猛然醒悟:Java的动态代理是1.6版本才支持,而当时我们使用的JDK版本乃是1.5!
在强烈的好奇心驱使下,我和同学反编译了连接池的JAR包,才发现原来是这样的!实现方法我们当时叫“静态代理”,就是实现IDbConnection接口(这里Java和C#混着讲,大概知道原理就行),然后手动写代码调用真实的IDbConnection对象,这得益于当初基于接口的设计。实现代码大概是这样的:
internal class ProxyConnection : IDbConnection { public ProxyConnection(ProxyConnectionPool pool, IDbConnection real) {
Pool = pool; Real = real; } public ProxyConnectionPool Pool { get; set; } public IDbconnection Real { get; set; } public IDbTransaction BeginTransaction() { return Real.BeginTransaction(); } // other implement... public void Dispose() { Close(); } public void Close() { Pool.Recycle(this); } }
依葫芦画瓢,基于OrmLite的实现大概是这样的:
internal class ProxyConnection : IDbConnection { public ProxyConnection(IOrmLiteDialectProvider provider, IDbConnection real) { Provider = provider; Real = real; } public IOrmLiteDialectProvider Provider { get; set; } public IDbconnection Real { get; set; } public IDbTransaction BeginTransaction() { return Real.BeginTransaction(); } }
Delete代码的修改:
// ============================== // WriteConnectionExtensions // ============================== public static int Delete<T>(this IDbConnection dbConn, Expression<Func<T, bool>> where) { var conn = dbConn as ProxyConnection; if( conn == null) throw new Exception("it's not a OrmLite DbConnection."); return dbConn.Exec(dbCmd => dbCmd.Delete(conn.Provider, where)); } public static int Delete<T>(this IDbCommand dbCmd, IOrmLiteDialectProvider provider, Expression<Func<T, bool>> where) { var ev = provider.SqlExpression<T>(); ev.Where(where); return dbCmd.Delete(ev); } //ReadConnectionExtensions public static T Exec<T>(this IDbConnection dbConn, Func<IDbCommand, T> filter) { var conn = dbConn as ProxyConnection; if( conn == null) throw new Exception("it's not a OrmLite DbConnection."); using (var dbCmd = conn.CreateCommand()) { dbCmd.Transaction = conn.Transaction; dbCmd.CommandTimeout = conn.CommandTimeout; var ret = filter(dbCmd); LastCommandText = dbCmd.CommandText; return ret; } }
例: SqliteOrmLiteDialectProvider.cs的修改
//修改前 public class SqliteOrmLiteDialectProvider : SqliteOrmLiteDialectProviderBase { public static SqliteOrmLiteDialectProvider Instance = new SqliteOrmLiteDialectProvider(); protected override IDbConnection CreateConnection(string connectionString) { return new SqliteConnection(connectionString); } } //修改后 public class SqliteOrmLiteDialectProvider : SqliteOrmLiteDialectProviderBase { public static SqliteOrmLiteDialectProvider Instance = new SqliteOrmLiteDialectProvider(); protected override IDbConnection CreateConnection(string connectionString) { var sqliteConn = new SqliteConnection(connectionString); return new ProxyConnection(this, sqliteConn); } }
如果修改成我的方案的话,其扩展方法接口不需修改,而且SQL生成再也不需要和线程耦合,转而与代理IDbConnection“耦合”,而这样的“耦合”也是理所当然的。我猜OrmLite之所以不这么做,其原因兼容“原始”的连接,在我的方案中,虽然扩展方法是面向IDbConnection,而实际上只面向ProxyConnection,如果非ProxyConnection的话会直接抛异常。
而实际上,OrmLite做了代理!(这是个整个思考的过程,所以没到最后,前面所下的“结论”不一定对。)
我们现在翻一下ReadConnectionExtensions.Exec<T> 方法:
public static T Exec<T>(this IDbConnection dbConn, Func<IDbCommand, T> filter) { var holdProvider = OrmLiteConfig.TSDialectProvider; try { var ormLiteDbConn = dbConn as OrmLiteConnection; if (ormLiteDbConn != null) OrmLiteConfig.TSDialectProvider = ormLiteDbConn.Factory.DialectProvider; using (var dbCmd = dbConn.CreateCommand()) { dbCmd.Transaction = (ormLiteDbConn != null) ? ormLiteDbConn.Transaction : OrmLiteConfig.TSTransaction; dbCmd.CommandTimeout = OrmLiteConfig.CommandTimeout; var ret = filter(dbCmd); LastCommandText = dbCmd.CommandText; return ret; } }
finally { OrmLiteConfig.TSDialectProvider = holdProvider;
} }
其中加粗红色部分就是代理类OrmLiteConnection,其作用是使用自带的DialectProvider代替线程上下文中的TSDialectProvider 。虽然是线程安全,但我个人不建议这种写法,而推荐其作为filter参数传入,大概如下:
public static T Exec<T>(this IDbConnection dbConn, Func<IDbCommand, IOrmLiteDialectProvider, T> filter) { using (var dbCmd = dbConn.CreateCommand()) { var ormLiteDbConn = dbConn as OrmLiteConnection; var holdProvider = (ormLiteDbConn != null) ? ormLiteDbConn.Factory.DialectProvider : OrmLiteConfig.TSDialectProvider; dbCmd.Transaction = (ormLiteDbConn != null) ? ormLiteDbConn.Transaction : OrmLiteConfig.TSTransaction; dbCmd.CommandTimeout = OrmLiteConfig.CommandTimeout; var ret = filter(dbCmd, holdProvider); LastCommandText = dbCmd.CommandText; return ret; } }
虽然filter调用时要麻烦点,但给人(我自认为)的感觉更安全,只有兼容到原始的IDbConnection时才需要和线程上下文相关,而原始的做法确实将原本独立的DialectProvider交给了上下文,我们再看看Exec的Action参数版本:
public static void Exec(this IDbConnection dbConn, Action<IDbCommand> filter) { var dialectProvider = OrmLiteConfig.DialectProvider; // (1) try { var ormLiteDbConn = dbConn as OrmLiteConnection; if (ormLiteDbConn != null) OrmLiteConfig.DialectProvider = ormLiteDbConn.Factory.DialectProvider; // (2) using (var dbCmd = dbConn.CreateCommand()) { dbCmd.Transaction = (ormLiteDbConn != null) ? ormLiteDbConn.Transaction : OrmLiteConfig.TSTransaction; dbCmd.CommandTimeout = OrmLiteConfig.CommandTimeout; filter(dbCmd); LastCommandText = dbCmd.CommandText; } } finally { OrmLiteConfig.DialectProvider = dialectProvider; // (3) } }
说实话,我不知道是代码BUG,还是我能力低没能理解到作者的意思。我们可以从第一篇回顾下OrmLiteConfig.DialectProvider的代码(两篇同时看)。
首先,我个人认为两个Exec方法中的dialectProvider 临时变量都是为了 简便filter 的内部实现的而暂时替代全局的OrmLiteConfig.(TS)DialectProvider(两个)变量。
在后一个实现方法中(Action参数)假设有两种情况:
1.当前线程用户没有自行设置TSDialectProvider,即TSDialectProvider = null。
2.TSDialectProvider 不为空。
情况1:
//(1) OrmLiteConfig.DialectProvider(get) = 默认,dialectProvider = 默认
//(2) OrmLiteConfig.DialectProvider(get) = 代理,dialectProvider = 默认
//(3) OrmLiteConfig.DialectProvider(get) = 默认,dialectProvider = 默认
情况2:
//(1) OrmLiteConfig.DialectProvider(get) = TSDialectProvider,dialectProvider = TSDialectProvider
//(2) OrmLiteConfig.DialectProvider(get) = TSDialectProvider,dialectProvider = TSDialectProvider,OrmLiteConfig.DialectProvider(set.dialectProvider) = 代理
//(3) OrmLiteConfig.DialectProvider(get) = TSDialectProvider,dialectProvider = TSDialectProvider,OrmLiteConfig.DialectProvider(set.dialectProvider) = TSDialectProvider
情况2中正因为OrmLiteConfig.DialectProvider优先返回TSDialectProvider才导致“数据现场无法恢复”。
但情况1就好了吗?如果项目中包含了3种不同数据库语言的存在,那么并发的时候也可能因乱序原因导致OrmLiteConfig.DialectProvider和最初的不一样。
怎么解决?很可能不需要解决,因为需要用到不同数据库语言的时候,根本不会再使用第一篇上的写法,这仅仅是我的代码洁癖,又或者担心有跟我一样不知道自己代码有多烂的程序员真的这样写而已。
那应该用怎样写法呢?请看下一篇。