转载请注明作者(think8848)和出处(http://think8848.cnblogs.com)
使用SqlCommand的感觉有时侯很爽,就跟那啥一样,对于数据的控制酣畅淋漓,但在这程中总是很担心一不小心打个颤,出现严重后果。之前在选择ORM时,选择了SubSonic,不觉已用了n年了,总的感觉来说还是非常不错的,但是SubSonic一直有一个硬伤:不能对同一个表进行JOIN连接。这个需求虽说不是天天有,但一个月总有那么几天需要去面对,搞的那几天人心情都不爽,当初选SubSonic是我力主的,解决不了问题,我难免得挨几下白眼。今天点低,又遇到了,需求很简单:一个Users中有ID,Name,SupervisorID三个列,SupervisorID为这个User的主管,很明显,这个查询很简单:
SELECT [Users].[ID],[Users].[Name],[Supervisors].[Name] AS [SupervisorName] FROM Users LEFT JOIN [Users] AS [Supervisors] ON [Users].[SupervisorID] = [Supervisors].[ID]
然而在SubSonic里不太简单了,最自然的写法为:
var query = new Select(/*Columns*/).From<User>()
.LeftOuterJoin<User>("SupervisorID","ID");
SubSonic生成的SQL为:
SELECT [Users].[ID], [Users].[Name], [Users].[SupervisorID] FROM INNER JOIN [Users] ON [Users].[SupervisorID] = [Users].[ID]
两个很离奇的结果,一个是在FROM后面居然没有表名,另一个是使用LeftOuterJoin方法,生成的居然是INNER JOIN?
于是使用LeftOuterJoin的另一个重载形式:
var provider = ProviderFactory.GetProvider();
var tbUsers = provider.FindOrCreateTable<User>();
var tbSupervisors = provider.FindOrCreateTable<User>();
var colSupervisorID = tbUsers.GetColumn("SupervisorID");
var colID = tbSupervisors.GetColumn("ID");
var query = new Select(/*Columns*/).From(tbUsers).LeftOuterJoin(colSupervisorID,colID);
这时SubSonic生成的SQL为:
SELECT [Users].[ID], [Users].[Name], [Users].[SupervisorID] FROM LEFT OUTER JOIN [Users] ON [Users].[SupervisorID] = [Users].[ID]
这次是LEFT OUTER JOIN了,但是FROM后面的表名还是没有,于是先查查在生成FROM时到底发生了什么
SubSonic.SqlGeneration.ANSISqlGenerator.cs
public virtual string GenerateFromList()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine();
sb.Append(this.sqlFragment.FROM);
bool isFirst = true;
foreach (ITable tbl in query.FromTables)
{
// EK: The line below is intentional. See: http://weblogs.asp.net/fbouma/archive/2009/06/25/linq-beware-of-the-access-to-modified-closure-demon.aspx
ITable table = tbl;
//Can't pop a table into the FROM list if it's also in a JOIN
if (!query.Joins.Any(x => x.FromColumn.Table.Name.Equals(table.Name, StringComparison.InvariantCultureIgnoreCase)))
{
if (!isFirst)
sb.Append(", ");
sb.Append(tbl.QualifiedName);
isFirst = false;
}
}
return sb.ToString();
}
原来它做了验证,如果FROM的表存在于将要JOIN的表中,则成生一个空字符串...
这样看来,SubSonic还真是没有提供这个功能了,现在的问题是,在SubSonic中添加这个功能的代价有多大呢,如果能轻量级(我喜欢轻量级)的解决这个问题还是值的动下手的。
先看看JOIN到底是怎么生成的:
SubSonic.SqlGeneration.ANSISqlGenerator.cs
public virtual string GenerateJoins()
{
StringBuilder sb = new StringBuilder();
if (query.Joins.Count > 0)
{
sb.AppendLine();
//build up the joins
foreach (Join j in query.Joins)
{
string joinType = Join.GetJoinTypeValue(this, j.Type);
string equality = " = ";
if (j.Type == Join.JoinType.NotEqual)
equality = " <> ";
sb.Append(joinType);
sb.Append(j.FromColumn.Table.QualifiedName);
if (j.Type != Join.JoinType.Cross)
{
sb.Append(" ON ");
sb.Append(j.ToColumn.QualifiedName);
sb.Append(equality);
sb.Append(j.FromColumn.QualifiedName);
}
}
}
return sb.ToString();
}
其中需要关注的是sb.Append(j.FromColumn.Table.QualifiedName);这句,SubSonic使用ITable的QualifiedName来生成JOIN后面的表名的,QualifiedName属性定义如下:
SubSonic.Schema.DatabaseTable.cs
public string QualifiedName
{
get { return Provider.QualifyTableName(this); }
}
SubSonic在需要表名的地方均使用了QualifiedName,在这种情况下,去修改QualifyTableName的值本身也不明智,而且这个有点“可恶”的是,定义个只读属性也就算了,居然值还是个方法的返回值,这种方式即使用反射也没有办法修改其值了,因此也就打消了在该属性动手脚的想法。到底该怎么办呢,手动添加一个Join吧,Join的实际上是一个IColumn,而IColumn的背后还站着一个ITable,看起来归根到底是需要生成一个ITable,而且这个ITable的名字不能和数据库中的表名相同(不然又被FROM给挡住了),最悲摧的是真实的表名还必须出现在SQL语句(有点废话)...
鉴于QualifiedName出现在多个地方,因此就使用QualifiedName作为别名吧,那么在sb.Append(j.FromColumn.Table.QualifiedName);这一行,QualifiedName肯定得换成诸如[XXX] AS QualifiedName之类的,只要动一行,就可以了。经过一番查看,发现DatabaseTable中的FriendlyName没有啥用,除了定义外没有发现任何地方有什么用,于是想出来一段代码:
public static SqlQuery SameTableJoin(this SqlQuery query, IColumn fromColumn, string toTableQualifiedName, Join.JoinType type, string toColumnName = "ID")
{
var provider = fromColumn.Provider;
var tmpTable = new DatabaseTable(toTableQualifiedName, provider);
tmpTable.FriendlyName = fromColumn.Table.Name;
var tmpCol = new DatabaseColumn(toColumnName, tmpTable);
query.Joins.Add(new Join(tmpCol, fromColumn, type));
if (!query.FromTables.Contains(tmpCol.Table))
{
query.FromTables.Add(tmpCol.Table);
}
return query;
}
FriendlyName是指定了,但是SubSonic并不知道我们用了这个属性啊,没办法,只有重载GenerateJoins方法了,在它里面使用FriendlyName,要达到非侵入目的,定义一个SqlServerProvider的派生类吧;
public class CleverSqlServerProvider : SqlServerProvider
{
public CleverSqlServerProvider(string connectionString, string providerName)
: base(connectionString, providerName)
{ }
public override ISqlGenerator GetSqlGenerator(SqlQuery query)
{
return new CleverSqlGenerator(query);
}
}
这个类其实还是没有做具体的SQL代码生成工作,还得定义一个SqlGenerator类:
public class CleverSqlGenerator : Sql2005Generator
{
public CleverSqlGenerator(SqlQuery query)
: base(query)
{
ClientName = "System.Data.SqlClient";
}
public override string GenerateJoins()
{
StringBuilder sb = new StringBuilder();
if (base.Query.Joins.Count > 0)
{
sb.AppendLine();
//build up the joins
foreach (Join j in base.Query.Joins)
{
string joinType = Join.GetJoinTypeValue(this, j.Type);
string equality = " = ";
if (j.Type == Join.JoinType.NotEqual)
equality = " <> ";
sb.Append(joinType);
sb.Append(string.IsNullOrEmpty(j.FromColumn.Table.FriendlyName) ? j.FromColumn.Table.QualifiedName : string.Format("[{0}] AS {1}", j.FromColumn.Table.FriendlyName, j.FromColumn.Table.QualifiedName));
if (j.Type != Join.JoinType.Cross)
{
sb.Append(" ON ");
sb.Append(j.ToColumn.QualifiedName);
sb.Append(equality);
sb.Append(j.FromColumn.QualifiedName);
}
}
}
return sb.ToString();
}
}
使用sb.Append(string.IsNullOrEmpty(j.FromColumn.Table.FriendlyName) ? j.FromColumn.Table.QualifiedName : string.Format("[{0}] AS {1}", j.FromColumn.Table.FriendlyName, j.FromColumn.Table.QualifiedName));一行,将FriendlyName应用了进去,现在唯一的问题是:如何使用CleverSqlServerProvider了,new一个吗?no no no,这个想都不要想,这是我不能容忍的,那种使用配置文件?好像还真没有发现该怎么配,再看看ProviderFactory类,发现一个有用的方法:
public static void Register(string providerName, Func<string, string, IDataProvider> factoryMethod)
{
if (_factories.ContainsKey(providerName))
{
_factories.Remove(providerName);
}
_factories.Add(providerName, factoryMethod);
}
这下有救了吧:)
使用SameTableJoin方法试试,看能不能生成想要的结果;
var provider = ProviderFactory.GetProvider();
var tbUser = provider.FindOrCreateTable<User>();
var colSupervisorID = tbUser.GetColumn("SupervisorID");
var query = new Select(/*Columns*/).From<User>()
.SameTableJoin(colSupervisorID, "Supervisors", Join.JoinType.LeftOuter);
看看生成的SQL:
SELECT [Users].[ID], [Users].[Name], [Supervisors].[Name] AS [SupervisorName] FROM [Users] LEFT OUTER JOIN [Users] AS [Supervisors] ON [Users].[SupervisorID] = [Supervisors].[ID]
OK,终于达到效果了,没有修改SubSonic的源码,但是达到了预期的目的。
写在后面的一句话,个人感觉,千万不要因为SubSonic这点瑕疵看不起它,总的来说,几年用下来还是觉得非常爽的,而且你也看到了,有了问题也很容易自已动手修补,我已经攒了不少这种扩展方法增强SubSonic的功能了。