Criteria 对象

当为 SELECT、 UPDATE 或 DELETE 创建动态 SQL 时,可能需要编写复杂的 WHERE 子句。

也可以使用拼接字符串构建这些语句,但避免语法错误是很繁琐的事且容易遭到 SQL 注入攻击。

使用参数化可以解决 SQL 注入问题,但为了添加参数需要过多的手动工作。

幸运的是,Serenity 有一个条件系统(criteria system),可以帮助你用类似 LINQ 表达式树的方式构建参数化的查询。

Serenity criterias 是通过 C# 的运算符(utilitizing operator)重载特性来实现的,而不像 LINQ 使用表达式树。

让我们首先在 where 子句写一个基本的 SQL 字符串:

  1. new SqlQuery()
  2. .From("MyTable")
  3. .Select("Name")
  4. .Where("Month > 5 AND Year < 2015 AND Name LIKE N'%a%'")

使用 criteria 对象实现相同的声明:

  1. new SqlQuery()
  2. .From("MyTable")
  3. .Select("Name")
  4. .Where(
  5. new Criteria("Month") > 5 &
  6. new Criteria("Year") < 4 &
  7. new Criteria("Name").Contains("a")

这看起来有点长,但它使用了参数:

  1. SELECT
  2. Name
  3. FROM
  4. MyTable
  5. WHERE
  6. Month > @p1 AND
  7. Year < @p2 AND
  8. Name LIKE N'%a%'

如果你有一个实体,你可以在智能提示的帮助下写该语句:

  1. var m = MyTableRow.Fields;
  2. new SqlQuery()
  3. .From(m)
  4. .Select(m.Name)
  5. .Where(
  6. m.Month > 5 &
  7. m.Year < 4 &
  8. m.Name.Contains("a")

我们这里没有使用 new Criteria(),因为 Field 对象已经有构建条件的重载操作。

BaseCriteria 对象

BaseCriteria 是所有条件(criteria)对象类型的基类。

它重载了几个 C# 操作运算符,包括 ><&|,它们可以用在 C# 表态式中,以构建复杂的条件。

BaseCriteria 自身没有构造函数,所以你需要创建一个从它派生的对象。Criteria 可能是最常使用的一个子类。

Criteria 对象

Criteria 是一个简单对象,包含 SQL 表达式的字符串,通常该字符串是一个字段名称。

  1. new Criteria("MyField")

它也可以包含一个 SQL 表达式(尽管不建议使用这种方式):

  1. new Criteria("a + b")

系统不会检查该参数的语法,因此构建的条件中可能含有无效的表达式。

  1. new Criteria("Some invalid expression()///''^')

AND (&) 操作

可以使用 C# 的 & 运算符对两个条件对象进行 与(AND) 运算:

  1. new Criteria("Field1 > 5") &
  2. new Criteria("Field2 < 4")
  3. `

请注意,我们这里不是使用短路运算符 && 。

它使用运算符 (AND) 创建新的条件对象(BinaryCriteria),并引用这两个条件对象。它并不修改原始的条件对象。

BinaryCriteria 类似于表达式树的二元表达式(BinaryExpression)。

它的 SQL 输出将是:

  1. Field1 > 5 AND Field2 < 4

也可以使用 C# 的 &= 运算符:

  1. BaseCriteria c = new Criteria("Field1 > 5)";
  2. c &= new Criteria("Field2 < 4")

BaseCriteria 是所有条件对象类型的基类。如果在第一行使用 Criteria c = …,第二行将得到编译时错误,因为 & 运算符返回 BinaryCriteria 对象,而不是返回 Criteria 对象。

OR (|) 操作

类似于 AND 操作,但它使用 OR。

  1. new Criteria("Field1 > 5") |
  2. new Criteria("Field2 < 4")
  3. `
  1. Field1 > 5 OR Field2 < 4

括号操作 (~)

当使用多个 AND/OR 子句,你可能想使用括号。

  1. new Criteria("Field1 > 5") &
  2. (new Criteria("Field2 > 7") | new Criteria("Field2 < 3"))

但是这不可以与条件对象一起工作,因为上述的条件将输出:

  1. Field1 > 5 AND Field2 > 7 OR Field2 < 3

这里的信息适用于 Serenity 1.9.8 之前的版本。在该版本之后,Serenity 在所有二元条件(AND 、OR 等)周围添加括号,即使你没有使用括号。

所以,如果你想在某些地方显式地使用括号,你只能使用 ~ 。

我们的括号被怎么处理?让我们试着添加更多的括号。

  1. new Criteria("Field1 > 5") &
  2. ((((new Criteria("Field2 > 7") | new Criteria("Field2 < 3")))))

一直还是输出:

  1. Field1 > 5 AND Field2 > 7 OR Field2 < 3

C# 没有提供重载括号的方法,它只被用来决定运算的顺序,因此,Serenity criteria 不能确定你是否在使用括号。

我们必须使用特殊的运算符:~(实际上它是 C# 的补码)。

  1. new Criteria("Field1 > 5") &
  2. ~(new Criteria("Field2 > 7") | new Criteria("Field2 < 3"))

现在 SQL 看起来像我们之前所希望的:

  1. Field1 > 5 AND (Field2 > 7 OR Field2 < 3)

由于 Serenity 1.9.8+ 自动向二元条件添加括号,上面的表达式事实上将变为:

  1. (Field1 > 5) AND (((Field2 > 7) OR (Field2 < 3)))

比较运算符 (>, >=, <, <=, ==, !=)

我们重载了大多数 C# 比较运算符,所以你可以在条件中使用它们。

  1. new Criteria("Field1") == new Criteria("1") &
  2. new Criteria("Field2") != new Criteria("2") &
  3. new Criteria("Field3") > new Criteria("3") &
  4. new Criteria("Field4") >= new Criteria("4") &
  5. new Criteria("Field5") < new Criteria("5") &
  6. new Criteria("Field6") <= new Criteria("6")
  1. Field1 == 1 AND
  2. Field2 <> 2 AND
  3. Field3 > 3 AND
  4. Field4 >= 4 AND
  5. Field5 < 5 AND
  6. Field6 <= 6

内联值(Inline Values)

当比较运算符的一侧是条件对象,而另一侧是整数、字符串、日期、guid 等值,这些值将被转换为条件参数。

  1. new Criteria("Field1") == 1 &
  2. new Criteria("Field2") != "ABC" &
  3. new Criteria("Field3") > DateTime.Now &
  4. new Criteria("Field4") >= Guid.NewGuid() &
  5. new Criteria("Field5") < 5L
  1. Field1 == @p1 AND
  2. Field2 <> @p2 AND
  3. Field3 > @p3 AND
  4. Field4 >= @p4 AND
  5. Field5 < @p5

当含有该条件的查询发送到 SQL,这些参数将具有相应的值。

自动参数编号默认从 1 开始,但最后一个编号存储在查询中,被条件使用,所以编号可能会变化。

让我们在查询中使用该条件:

  1. new SqlQuery()
  2. .From("MyTable")
  3. .Select("Field999")
  4. .Where(new Criteria("FirstOne") >= 999)
  5. .Where(new Criteria("SecondOne") >= 999)
  6. .Where(
  7. new Criteria("Field1") == 1 &
  8. new Criteria("Field2") != "ABC" &
  9. new Criteria("Field3") > DateTime.Now &
  10. new Criteria("Field4") >= Guid.NewGuid() &
  11. new Criteria("Field5") < 5L
  12. )
  1. SELECT
  2. Field999
  3. FROM
  4. MyTable
  5. WHERE
  6. FirstOne >= @p1 AND -- @p1 = 999
  7. SecondOne >= @p2 AND -- @p2 = 999
  8. Field1 == @p3 AND -- @p3 = 1
  9. Field2 <> @p4 AND -- @p4 = N'ABC'
  10. Field3 > @p5 AND -- @p5 = '2016-01-31T01:16:23'
  11. Field4 >= @p6 AND -- @p6 = '23123-DEFCD-....'
  12. Field5 < @p7 -- @p7 = 5

这里是与之前列出表达式有相同的条件,参数编号从 3 而不是 1 开始。因为 2 之前的编号被用于其他 WHERE 子句。

所以参数编号使用查询作为上下文。你不应该做出参数名称将是什么的假设。

ParamCriteria 和显式参数名

如果想要使用一些显式命名的参数,你可以使用 ParamCriteria:

  1. new SqlQuery()
  2. .From("SomeTable")
  3. .Select("SomeField")
  4. .Where(new Criteria("SomeField") <= new ParamCriteria("@myparam"))
  5. .Where(new Criteria("SomeOtherField") == new ParamCriteria("@myparam"))
  6. .SetParam("@myparam", 5);

我们这里使用 SqlQuery 的 SetParam 扩展方法设置参数的值。

也可以事先声明此参数,然后重用它:

  1. var myParam = new ParamCriteria("@myparam");
  2. new SqlQuery()
  3. .From("SomeTable")
  4. .Select("SomeField")
  5. .Where(new Criteria("SomeField") <= myParam)
  6. .Where(new Criteria("SomeOtherField") == myParam)
  7. .SetParam(myParam.Name, 5);

ConstantCriteria

如果你不想使用参数化的查询,可以使用 ConstantCriteria 对象存储值。它们将不会转换为自动参数。

  1. new SqlQuery()
  2. .From("MyTable")
  3. .Select("MyField")
  4. .Where(
  5. new Criteria("Field1") == new ConstantCriteria(1) &
  6. new Criteria("Field2") != new ConstantCriteria("ABC")
  7. )
  1. SELECT
  2. MyField
  3. FROM
  4. MyTable
  5. WHERE
  6. FirstOne >= 1
  7. SecondOne >= N'ABC'

Null 比较

在 SQL 中,使用像 ==,!= 的运算符比较 NULL 值,将返回 NULL。对于这样的比较,应使用 IS NULL 或 IS NOT NULL。

Criteria 对象没有重载防止 null(或 object)的比较,所以如果你尝试写下面的表达式,可能会得到错误:

  1. new Criteria("a") == null; // what is type of null?
  2. int b? = null;
  3. new Criteria("c") == b; // no overload for nullable types

这些表达式都可以使用 IsNull 和 Nullable.Value 方法编写:

  1. new Criteria("a").IsNull();
  2. new Criteria("a").IsNotNull();
  3. int? b = 5;
  4. new Criteria("c") == b.Value;

如果你迫切希望这样写:Field = NULL,可以这样做:

  1. new Criteria("Field") == new Criteria("NULL")

LIKE 操作

Criteria 有 Like, NotLike, StartsWith, EndsWith, Contains, NotContains 方法,以帮助使用 LIKE 操作。

  1. new Criteria("a").Like("__C%") &
  2. new Criteria("b").NotLike("D%") &
  3. new Criteria("c").StartsWith("S") &
  4. new Criteria("d").EndsWith("X") &
  5. new Criteria("e").Contains("This") &
  6. new Criteria("f").NotContains("That")
  1. a LIKE @p1 AND -- @p1 = N'__C%'
  2. b NOT LIKE @p2 AND -- @p2 = N'D%'
  3. c LIKE @p3 AND -- @p3 = 'S%'
  4. d LIKE @p4 AND -- @p4 = N'%X'
  5. e LIKE @p5 AND -- @p5 = N'%This%'
  6. f NOT LIKE @p6 -- @p6 = N'%That%'

IN 和 NOT IN 操作

在内联数组中使用 IN 或 NOT IN:

  1. new Criteria("A").In(1, 2, 3, 4, 5)
  1. A IN (@p1, @p2, @p3, @p4, @p5)
  2. -- @p1 = 1, @p2 = 2, @p3 = 3, @p4 = 4, @p5 = 5
  1. new Criteria("A").NotIn(1, 2, 3, 4, 5)
  1. A NOT IN (@p1, @p2, @p3, @p4, @p5)
  2. -- @p1 = 1, @p2 = 2, @p3 = 3, @p4 = 4, @p5 = 5

也可以向 IN 方法传递任何可枚举的参数:

  1. IEnumerable<int> x = new int[] { 1, 3, 5, 7, 9 };
  2. new Criteria("A").In(x);
  1. A IN (1, 3, 5, 7, 9)
  2. -- @p1 = 1, @p2 = 3, @p3 = 5, @p4 = 7, @p5 = 9

也可以使用子查询:

  1. var query = new SqlQuery()
  2. .From("MyTable")
  3. .Select("MyField");
  4. query.Where("SomeID").In(
  5. query.SubQuery()
  6. .From("SomeTable")
  7. .Select("SomeID")
  8. .Where(new Criteria("Balance") < 0));
  1. SELECT
  2. MyField
  3. FROM
  4. MyTable
  5. WHERE
  6. SomeID IN (
  7. SELECT
  8. SomeID
  9. FROM
  10. SomeTable
  11. WHERE
  12. Balance < @p1 -- @p1 = 0
  13. )

NOT 操作

使用 C# 的 ! (not) 运算符表示 NOT 运算操作:

  1. !(new Criteria("a") >= 5)
  1. NOT (a >= @p1) -- @p1 = 5

Field 对象的用法

到目前为止,我们已经使用 Criteria 对象构造函数来构建条件。Field 对象也有类似的重载,所以它们可以相互替代。

例如,以 Northwind 的 Order、 Detail 和 Customer 行(row)为例:

  1. var o = OrderRow.Fields.As("o");
  2. var od = OrderDetailRow.Fields.As("od");
  3. var c = CustomerRow.Fields.As("c");
  4. var query = new SqlQuery()
  5. .From(o)
  6. .Select(o.CustomerID);
  7. query.Where(
  8. o.CustomerCountry == "France" &
  9. o.ShippingState == 1 &
  10. o.CustomerID.In(
  11. query.SubQuery()
  12. .From(c)
  13. .Select(c.CustomerID)
  14. .Where(c.Region == "North")) &
  15. new Criteria(
  16. query.SubQuery()
  17. .From(od)
  18. .Select(Sql.Sum(od.LineTotal.Expression))
  19. .Where(od.OrderID == o.OrderID)) >= 1000);

它将输出:

  1. SELECT
  2. o.CustomerID AS [CustomerID]
  3. FROM
  4. Orders o
  5. LEFT JOIN
  6. Customers o_c ON (o_c.CustomerID = o.CustomerID)
  7. WHERE
  8. o_c.[Country] = @p2
  9. AND (CASE WHEN
  10. o.[ShippedDate] IS NULL THEN 0
  11. ELSE 1
  12. END) = @p3
  13. AND o.CustomerID IN (
  14. SELECT
  15. c.CustomerID AS [CustomerID]
  16. FROM
  17. Customers c
  18. WHERE
  19. c.Region = @p1)
  20. AND (SELECT
  21. SUM((od.[UnitPrice] * od.[Quantity] - od.[Discount]))
  22. FROM
  23. [Order Details] od
  24. WHERE
  25. od.OrderID = o.OrderID) >= @p4