详细解说NParsing框架实现原理 —— 2)参数组件(ObParameter)
通常的三层架构的代码中,为了方便,大家往往会把WHERE条件的字符串放到业务逻辑层去拼接。这样会带来以下几个问题:
1、没有参数化,只用字符拼接的WHERE条件,非常不安全,容易被SQL注入。
2、在写程序时,随意性太大,程序不够严谨。比如一开始是查询一个表,条件中没加表名。后来由于业务需要,要对多个表关连,改了数据持久层,结果运行肯定出错。
3、在多表关连查询时,条件又非常复杂时,拼接的WHERE条件字符串巨长。看得头晕眼花。
4、程序移植性太差。不同数据库之间,虽然大部分SQL语法是相同的,但也还有小部分的东西不一样,你把WHERE条件写死在业务层了,哪天要换个数据库,还要重写一套业务逻辑不成?
NParsing框架中的参数组件(ObParameter)就是为了解决这些问题,以定义简单、标准的操作接口,实现不同数据库WHERE条件生成。
使用参数化、保证程序员书写严谨、书写格式简单易懂、关键是移植性强。
参数组件,实际上也就是一个封装了拼接SQL语句中WHERE条件方法组件。提供统一的调用方法。
接口如下:
2 using System.Data.Common;
3
4 namespace DotNet.Frameworks.NParsing.Interface
5 {
6 public interface IObParameter
7 {
8 /// <summary>
9 /// 值 null, DbTerm, DbNTerm
10 /// </summary>
11 object Value { get; set; }
12
13 /// <summary>
14 /// 平级兄弟列表
15 /// </summary>
16 IList<IObParameter> Brothers { get; set; }
17
18 /// <summary>
19 /// 0 无兄弟 1 AND 2 OR
20 /// </summary>
21 int BrotherType { get; set; }
22
23 /// <summary>
24 /// SQL条件语句
25 /// </summary>
26 string ToString(ref IList<DbParameter> dbParameters);
27
28 /// <summary>
29 /// 平级AND条件
30 /// </summary>
31 /// <param name="iObParameter"></param>
32 /// <returns></returns>
33 IObParameter And(IObParameter iObParameter);
34
35 /// <summary>
36 /// 平级OR条件
37 /// </summary>
38 /// <param name="iObParameter"></param>
39 /// <returns></returns>
40 IObParameter Or(IObParameter iObParameter);
41 }
42 }
在参数组件实现中,需要解决以下问题:
1、解决特殊条件,比如IS NULL,NOT IS NULL。
2、解决条件中括号的运用。
3、解决书写时程序的严谨性。
4、解决生成参数化的WHERE条件。
5、解决WHERE条件延迟生成。
一、如何解决特殊条件(IS NULL或NOT IS NULL)的处理。
普通单条件的组成,有“表名”、“字段名”、“条件符号”、“值”四个部分。
如 Table1.column1=@value1
特殊单条件的组成,有“表名”、“字段名”、“IS NULL或NOT IS NULL”三个部分。
如 Table1.column1 IS NULL
这里有两个东西是可以固定的,由用户选的。“条件符号”和“IS NULL或NOT IS NULL”。所以我定义了两个枚举。
条件符号(DbSymbol)代码如下:
2 {
3 public enum DbSymbol
4 {
5 Equal, //=
6 NotEqual, //<>
7 LessEqual, //<=
8 ThanEqual, //>=
9 Less, //>
10 Than, //<
11 Like,
12 LikeLeft,
13 LikeRight,
14 In,
15 NotIn
16 }
17 }
特殊条件值(DbValue)代码如下:
2 {
3 public enum DbValue
4 {
5 IsNull,
6 NotIsNull
7 }
8 }
创建普通条件代码:
2 /// 创建数据库操作接口,指定connectionStringName数据库连接配置结点名称
3 /// </summary>
4 /// <typeparam name="M">对象模型</typeparam>
5 /// <param name="connectionStringName">数据库连接配置结点名称</param>
6 /// <returns></returns>
7 public static IObHelper<M> Create<M>(string connectionStringName)
8 {
9 var connectionStringSettings = ConfigurationManager.ConnectionStrings[connectionStringName];
10 #if (DEBUG)
11 if (connectionStringSettings == null)
12 {
13 throw new Exception(string.Format("数据库连接配置节点{0}未找到", connectionStringName));
14 }
15 #endif
16 Type t = typeof (M);
17 var className = CLASS_NAME + "[[" + t.FullName + "," + t.Assembly.FullName + "]]";
18 t = Assembly.Load(ASSEMBLY_STRING).GetType(className);
19 var parameters = new[]
20 {
21 connectionStringSettings.ConnectionString,
22 connectionStringSettings.ProviderName
23 };
24 return (IObHelper<M>) Activator.CreateInstance(t, parameters);
25 }
创建特殊条件代码:
2 /// 创建数据库操作接口,传入数据库连接字符串和数据库操作类库名称,方便数据库连接字符串加密存储
3 /// </summary>
4 /// <typeparam name="M">对象模型</typeparam>
5 /// <param name="connectionString">数据库连接字符串</param>
6 /// <param name="providerName">数据库操作类库名称</param>
7 /// <returns></returns>
8 public static IObHelper<M> Create<M>(string connectionString, string providerName)
9 {
10 Type t = typeof(M);
11 var className = CLASS_NAME + "[[" + t.FullName + "," + t.Assembly.FullName + "]]";
12 t = Assembly.Load(ASSEMBLY_STRING).GetType(className);
13 var parameters = new[]
14 {
15 connectionString,
16 providerName
17 };
18 return (IObHelper<M>)Activator.CreateInstance(t, parameters);
19 }
二、如何解决条件中括号的运用?
整个WHERE条件字符串中,因为优先级的关系,必须要用到括号。
在And、Or方法中我是这样实现的,每个IObParameter实例都有一个平级条件列表(Brothers)。在当前参数实例的Brothers列表中的参数实例,都中本参数实例是平级的(这句话有绕,大家理解理解吧)。如果Brothers例表中有一个以上参数实例时,就会把本参数实例和Brothers列表中的参数实例用括号括起来。具体实现如下代码(SQLServer):
2 /// 创建Where条件语句
3 /// </summary>
4 /// <param name="iObParameter">参数</param>
5 /// <param name="dbParameter">回带数据库参数</param>
6 /// <returns></returns>
7 private static string CreateWhere(IObParameter iObParameter, ref IList<DbParameter> dbParameter)
8 {
9 string sqlWhere = string.Empty;
10 if(iObParameter.Value is DbTerm)
11 {
12 sqlWhere = CreateSymbolWhere(iObParameter, ref dbParameter);
13 }
14 else if(iObParameter.Value is DbNTerm)
15 {
16 sqlWhere = CreateValueWhere(iObParameter);
17 }
18 int iBrotherCount = iObParameter.Brothers.Count;
19 for (int i = 0; i < iBrotherCount; i++)
20 {
21 var brother = iObParameter.Brothers[i];
22 switch (brother.BrotherType)
23 {
24 case 1:
25 sqlWhere += " AND ";
26 break;
27 case 2:
28 sqlWhere += " OR ";
29 break;
30 }
31 string andorWhere = "{0}";
32 if (iObParameter.Brothers[i].Brothers.Count > 0)
33 {
34 andorWhere = "(" + andorWhere + ")";
35 }
36 sqlWhere += string.Format(andorWhere, CreateWhere(brother, ref dbParameter));
37 }
38 return sqlWhere;
39 }
整个条件使用括号,在构造中实现。代码如下:
2 private const string CLASS_NAME = ASSEMBLY_STRING + ".ObParameter";
3 /// <summary>
4 /// 创建子条件
5 /// </summary>
6 /// <param name="iObParameter">参数</param>
7 /// <returns></returns>
8 public static IObParameter Create(IObParameter iObParameter)
9 {
10 Type t = Assembly.Load(ASSEMBLY_STRING).GetType(CLASS_NAME);
11 return (IObParameter)Activator.CreateInstance(t, iObParameter);
12 }
三、如何解决书写时程序的严谨性?
上面说过,
普通单条件的组成,有“表名”、“字段名”、“条件符号”、“值”四个部分。
如 Table1.column1=@value1
特殊单条件的组成,有“表名”、“字段名”、“条件符号”、“值”、“IS NULL或NOT IS NULL”三个部分。
如 Table1.column1 IS NULL
“表名”、“字段名”、“条件符号”、“IS NULL或NOT IS NULL”都是要外部传入的。怎么保证用户书写准确。我是这么做的:
public static IObParameter Create<M>(string propertyName, DbValue dbValue)
public static IObParameter Create<M>(string propertyName, DbSymbol dbSymbol, object value)
表名用对象模型以范型方式传入,
字段名以字符串参数方式传入,
“条件符号”、特殊条件值“IS NULL或NOT IS NULL”,用枚举定义,
在创建时验正M对象模型中有没有propertyName属性,以保正条件准确性。
四、如何解决生成参数化的WHERE条件?
参数化要注意的是,防止参数重名。如在UPDATE语句中,可能这个字段既是数据更新字段,也是条件字段。
以SQL Server代码为例:
2 /// 创建带符号的Where条件语句
3 /// </summary>
4 /// <param name="iObParameter">参数</param>
5 /// <param name="dbParameter">回带数据库参数</param>
6 /// <returns></returns>
7 private static string CreateSymbolWhere(IObParameter iObParameter, ref IList<DbParameter> dbParameter)
8 {
9 string sqlWhere = string.Empty;
10 var dbTerm = (DbTerm)iObParameter.Value;
11 string parameterName = "@" + dbTerm.ColumnName;
12
13 #region 防止重复参数名
14
15 int i = 0;
16 foreach (var parameter in dbParameter)
17 {
18 var pn = parameter.ParameterName;
19 if (pn.Length > parameterName.Length && pn.Substring(0, parameterName.Length).Equals(parameterName))
20 i++;
21 else if (pn.Length == parameterName.Length && pn.Equals(parameterName))
22 i++;
23 }
24 parameterName += i == 0 ? "" : i.ToString();
25
26 #endregion
27
28 switch (dbTerm.DbSymbol)
29 {
30 case DbSymbol.Equal:
31 sqlWhere += string.Format("{0}.{1} = {2}", dbTerm.TableName, dbTerm.ColumnName, parameterName);
32 break;
33 case DbSymbol.NotEqual:
34 sqlWhere += string.Format("{0}.{1} <> {2}", dbTerm.TableName, dbTerm.ColumnName, parameterName);
35 break;
36 case DbSymbol.LessEqual:
37 sqlWhere += string.Format("{0}.{1} <= {2}", dbTerm.TableName, dbTerm.ColumnName, parameterName);
38 break;
39 case DbSymbol.ThanEqual:
40 sqlWhere += string.Format("{0}.{1} >= {2}", dbTerm.TableName, dbTerm.ColumnName, parameterName);
41 break;
42 case DbSymbol.Less:
43 sqlWhere += string.Format("{0}.{1} < {2}", dbTerm.TableName, dbTerm.ColumnName, parameterName);
44 break;
45 case DbSymbol.Than:
46 sqlWhere += string.Format("{0}.{1} > {2}", dbTerm.TableName, dbTerm.ColumnName, parameterName);
47 break;
48 case DbSymbol.Like:
49 case DbSymbol.LikeLeft:
50 case DbSymbol.LikeRight:
51 sqlWhere += string.Format("{0}.{1} LIKE {2}", dbTerm.TableName, dbTerm.ColumnName, parameterName);
52 break;
53 case DbSymbol.In:
54 sqlWhere += string.Format("{0}.{1} IN ({2})", dbTerm.TableName, dbTerm.ColumnName, parameterName);
55 break;
56 case DbSymbol.NotIn:
57 sqlWhere += string.Format("{0}.{1} NOT IN ({2})", dbTerm.TableName, dbTerm.ColumnName, parameterName);
58 break;
59 }
60 switch (dbTerm.DbSymbol)
61 {
62 case DbSymbol.Like:
63 dbParameter.Add(new SqlParameter(parameterName, "%" + dbTerm.Value + "%"));
64 break;
65 case DbSymbol.LikeLeft:
66 dbParameter.Add(new SqlParameter(parameterName, "%" + dbTerm.Value));
67 break;
68 case DbSymbol.LikeRight:
69 dbParameter.Add(new SqlParameter(parameterName, dbTerm.Value + "%"));
70 break;
71 default:
72 dbParameter.Add(new SqlParameter(parameterName, dbTerm.Value));
73 break;
74 }
75 return sqlWhere;
76 }
五、什么是WHERE条件延迟生成?
就是不在创建ObParameter时生成WHERE字符串,而是在控制器组件调用具体操作方法时生成。好处是,不用在创建ObParameter时,告诉它我是要生成什么类型数据库的WHERE条件。
注:NParsing框架及Demo程序,将会在“引言(NParsing框架功能简介、NParsing的由来) ”中提供下载。