Criteria 对象

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

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

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

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

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

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

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

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

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

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

SELECT 
    Name
FROM
    MyTable
WHERE
    Month > @p1 AND
    Year < @p2 AND
    Name LIKE N'%a%'

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

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

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

BaseCriteria 对象

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

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

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

Criteria 对象

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

new Criteria("MyField")

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

new Criteria("a + b")

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

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

AND (&) 操作

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

new Criteria("Field1 > 5") &
new Criteria("Field2 < 4")
`

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

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

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

它的 SQL 输出将是:

Field1 > 5 AND Field2 < 4

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

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

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

OR (|) 操作

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

new Criteria("Field1 > 5") |
new Criteria("Field2 < 4")
`
Field1 > 5 OR Field2 < 4

括号操作 (~)

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

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

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

Field1 > 5 AND Field2 > 7 OR Field2 < 3

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

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

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

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

一直还是输出:

Field1 > 5 AND Field2 > 7 OR Field2 < 3

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

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

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

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

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

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

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

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

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

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

内联值(Inline Values)

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

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

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

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

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

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

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

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

ParamCriteria 和显式参数名

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

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

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

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

var myParam = new ParamCriteria("@myparam");

new SqlQuery()
    .From("SomeTable")
    .Select("SomeField")
    .Where(new Criteria("SomeField") <= myParam)
    .Where(new Criteria("SomeOtherField") == myParam)
    .SetParam(myParam.Name, 5);

ConstantCriteria

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

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

Null 比较

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

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

new Criteria("a") == null; // what is type of null?

int b? = null;
new Criteria("c") == b; // no overload for nullable types

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

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

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

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

LIKE 操作

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

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

IN 和 NOT IN 操作

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

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

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

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

也可以使用子查询:

var query = new SqlQuery()
    .From("MyTable")
    .Select("MyField");

query.Where("SomeID").In(
    query.SubQuery()
        .From("SomeTable")
        .Select("SomeID")
        .Where(new Criteria("Balance") < 0));
SELECT 
  MyField
FROM
  MyTable
WHERE
  SomeID IN (
    SELECT 
        SomeID 
    FROM
        SomeTable
    WHERE
        Balance < @p1 -- @p1 = 0
  )

NOT 操作

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

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

Field 对象的用法

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

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

    var o = OrderRow.Fields.As("o");
    var od = OrderDetailRow.Fields.As("od");
    var c = CustomerRow.Fields.As("c");
    var query = new SqlQuery()
        .From(o)
        .Select(o.CustomerID);

    query.Where(
        o.CustomerCountry == "France" &
        o.ShippingState == 1 &
        o.CustomerID.In(
            query.SubQuery()
                .From(c)
                .Select(c.CustomerID)
                .Where(c.Region == "North")) &
        new Criteria(
            query.SubQuery()
                .From(od)
                .Select(Sql.Sum(od.LineTotal.Expression))
                .Where(od.OrderID == o.OrderID)) >= 1000);

它将输出:

SELECT 
    o.CustomerID AS [CustomerID] 
FROM 
    Orders o 
LEFT JOIN 
    Customers o_c ON (o_c.CustomerID = o.CustomerID) 
WHERE 
    o_c.[Country] = @p2
    AND (CASE WHEN 
        o.[ShippedDate] IS NULL THEN 0
     ELSE 1 
     END) = @p3 
    AND o.CustomerID IN (
        SELECT 
            c.CustomerID AS [CustomerID] 
        FROM 
            Customers c 
        WHERE 
            c.Region = @p1) 
    AND (SELECT 
        SUM((od.[UnitPrice] * od.[Quantity] - od.[Discount])) 
     FROM
        [Order Details] od 
     WHERE 
        od.OrderID = o.OrderID) >= @p4

results matching ""

    No results matching ""