Dapper使用参数列表进行查询

20

我正在尝试使用Dapper运行一个已知参数集合的查询,但是这些参数需要使用值列表。一个简单的例子是:

DateTime endDate = DateTime.Now;
DateTime startDate = endDate.AddHours(-24);

string query = "select COUNT(*) from Test where Status = @Status AND DateCreated <= @Hour;";
var stuff = con.Query(query, (startDate).ByHourTo(endDate).Select(hour => new
{
     Status = 1,
     Hour = hour,
}));

Dapper抛出了一个异常,显示“必须定义参数'@Status'”。我知道在执行批量插入和更新时,Dapper可以处理参数列表,但是它不能用于查询吗?


3
你是否在dapper首页上看到了自定义的“in”示例?这确实是一个得到了很好支持的场景。 - Marc Gravell
@Marc,是的,我知道如何使用“IN”轻松地使用Dapper执行上面的示例。让我修改我的问题,希望能够说明为什么在我的情况下“IN”不起作用。我试图概括我想要做的事情,但显然我的示例过于简单。 - Jarrod
@Marc,我已经修改了示例,使其更接近我实际尝试的内容,即在一段时间内生成报告数据。 - Jarrod
Dapper支持在插入操作中使用列表的原因是每个插入都是单独的命令。你真的想执行24个单独的选择吗?我用日期范围分区数据的方法是生成一个带有我的范围的临时表并加入它。这只是普通的T-SQL,与Dapper无关,但Dapper显然可以为你映射最终选择的结果。如果我有时间,我会添加一个示例作为答案。我知道这是一个三年前的问题,但我从谷歌上找到了这里。其他人可能会欣赏这个答案。 - Glazed
我履行了承诺,添加了我的答案。 - Glazed
4个回答

38

试一试:

List<string> names = new List<string> { "Bob", "Fred", "Jack" };
string query = "select * from people where Name in @names";
var stuff = connection.Query<ExtractionRecord>(query, new {names});

如我在原问题中所述,ID在我的查询中无法工作。请参见上面的注释。 - Jarrod
看起来你的问题现在有几个错误,但你的意图不是很清楚(例如,你正在将DateCreated与@Hour进行比较?)。我不太确定你卡在哪里了。首先阅读Dapper文档,你没有正确传递参数。 - D'Arcy Rittich
我认为马克的答案验证了我的结论,即在Execute和Query之间,Dapper处理参数的方式存在差异。我进行了额外的编辑,希望更加清晰明了。 - Jarrod
1
@Jarrod 我认为提供样本数据和期望输出会有所帮助。你可能可以使用GROUP BY来获得你想要的结果。 - D'Arcy Rittich
1
如果你想在 PostgreSQL 中使用它,并且收到类似于“operator does not exist: integer = integer[]”的错误,请尝试这样做:select * from people where Name = ANY(@names) - rotman
显示剩余4条评论

12

啊,我想我知道你的意思了...

是的,我们支持Execute的一种场景,而这种场景对于Query来说是不支持的,具体来说:使用一系列不同的参数值依次运行相同的操作。这在Execute中是有意义的,但对于查询而言,这可能意味着您应该考虑使用in来进行不同参数的查询。或者,只需循环和串联。

相反,它正在查看单个参数对象,并寻找公共值 - 枚举没有任何适合Dapper的参数值。


是的,这就是我所担心的。我的实际查询非常复杂,但涉及生成结果集,其中每一行包含基于时间戳计算各种东西的计数。时间戳必须在几个 JOIN 中使用,因此我认为没有任何方法可以使用 IN。我不知道有什么方法可以创建一个 SQL 查询,可以在两个时间戳之间选择每个小时,因此我不得不使用 C# 来生成它们。我的当前解决方案执行多个独立的查询,就像您建议的那样。谢谢! - Jarrod
如果Dapper能够将IEnumerable参数转换为派生表(用于JOINs),那就太棒了。列名可以基于参数名称。 - D'Arcy Rittich
@RedFilter 你的意思是像一个表变量吗? - Marc Gravell
@MarcGravell 我在考虑一个值列表(例如,1,2,3),它会被转换为:(select 1 as val union all select 2 union all select 3)。语法可以是 select * from MyTable t inner join @vals v on t.id = v.val - D'Arcy Rittich
1
@RedFilter,你可以使用专门的“in”来实现这一点 - “where t.id in @vals”,将new { vals }作为参数传递。 - Marc Gravell
显示剩余3条评论

4
我知道我来晚了,但是我认为你的请求是想要传入一些属性并根据这些动态属性生成查询。通过下面的代码,我可以使用任何类型,然后只需填充和传递一个该类型的对象,并设置几个值(我称之为查询对象),查询将被生成以查找与您在查询对象中设置的值匹配的对象。*注意bool和具有默认值的内容。以下是动态查询示例:
    public IEnumerable<T> Query<T>(T templateobject) {
        var sql = "SELECT * From " + typeof(T).Name + " Where ";

        var list = templateobject.GetType().GetProperties()
             .Where(p => p.GetValue(templateobject) != null)
             .ToList();

        int i = 0;

        Dictionary<string, object> dbArgs = new Dictionary<string, object>();

        list.ForEach(x =>
        {
            sql += x.Name + " = @" +  x.Name;

            dbArgs.Add(x.Name, x.GetValue(templateobject));

            if (list.Count > 1 && i < list.Count - 1) {
                sql += " AND ";
                i++;
            }
        });

        Debug.WriteLine(sql);

        return _con.Query<T>(sql, dbArgs).ToList();
    }

用法

*repo是包含上述函数的类

var blah = repo.Query<Domain>(new Domain() { Id = 1, IsActive=true });

输出

SELECT * From Domain Where Id = @Id AND IsActive = @IsActive

然后它会返回与上述查询匹配的任何“域名”。

不要纠结于何时添加AND,尝试使用string.Join(" AND ", listOfParamEqualValue)。我仍然建议避免使用这种模式,因为它使用了动态SQL,可能会增加后期误用或滥用的风险。 - StingyJack

0
DECLARE @Now datetime
SET @Now = getdate()

SELECT
    DATEADD( hh, -n, @Now ) AS StartDate,
    DATEADD( hh, -n+1, @Now ) AS EndDate
INTO
    #DateRanges
FROM 
    Numbers
WHERE
    n <= 24

SELECT
    COUNT(*) AS [Count],
    #DateRanges.StartDate
FROM
    Test
        JOIN
    #DateRanges
        ON Test.DateCreated >= #DateRanges.StartDate
        AND Test.DateCreated < #DateRanges.EndDate
GROUP BY
    #DateRanges.StartDate

这是我会做的方式,但前提是:你的数据库中有一张名为“Numbers”的表格,其中包含任意数量的整数,每行一个,从1开始,至少有24个数字。
也就是说,这个表格看起来像这样:
n
-----
1
2
3
4
5
...

如果你没有这样的表,那么为了这个命令创建一个表非常快速和简单:

CREATE TABLE #Numbers
(
    n int
)

SET NOCOUNT ON

INSERT #Numbers values (1);
GO
INSERT #Numbers SELECT n + (SELECT COUNT(*) FROM #Numbers) FROM #Numbers
GO 16 --execute batch 16 times to create 2^16 integers.

在存储过程中,您不能有多个批处理,但可以在文本命令中使用。 GO 16 运行前面的批处理 16 次。如果您需要在存储过程中使用此功能,可以重复第二个 INSERT 命令多次,而不是使用批处理。对于这个特定的查询来说,2^16 个整数已经足够了,但当需要时,我会复制并粘贴这个命令,2^16 通常已经足够快,以至于我通常不会改变它。 GO 5 将产生 32 个整数,这足以覆盖 24 个日期范围。

下面是一个完整的脚本,演示了这个功能的工作方式:

--Create a temp table full of integers. This could also be a static 
--table in your DB. It's very handy.
--The table drops let us run this whole script multiple times in SSMS without issue.
IF OBJECT_ID( 'tempdb..#Numbers' ) IS NOT NULL
    DROP TABLE #Numbers

CREATE TABLE #Numbers
(
    n int
)

SET NOCOUNT ON

INSERT #Numbers values (1);
GO
INSERT #Numbers SELECT n + (SELECT COUNT(*) FROM #Numbers) FROM #Numbers
GO 16 --execute batch 16 times to create 2^16 integers.

--Create our Test table. This would be the real table in your DB, 
-- so this would not go into your SQL command.
IF OBJECT_ID( 'tempdb..#Test' ) IS NOT NULL
    DROP TABLE #Test

CREATE TABLE #Test
(
    [Status] int,
    DateCreated datetime
)

INSERT INTO 
    #Test 
SELECT 
    1, 
    DATEADD( hh, -n, getdate() )
FROM 
    #Numbers
WHERE
    n <= 48

--#Test now has 48 records in it with one record per hour for 
--the last 48 hours.

--This drop would not be needed in your actual command, but I 
--add it here to make testing this script easier in SSMS.
IF OBJECT_ID( 'tempdb..#DateRanges' ) IS NOT NULL
    DROP TABLE #DateRanges

--Everything that follows is what would be in your SQL you send through Dapper 
--if you used a static Numbers table, or you might also want to include
--the creation of the #Numbers temp table.
DECLARE @Now datetime
SET @Now = getdate()

SELECT
    DATEADD( hh, -n, @Now ) AS StartDate,
    DATEADD( hh, -n+1, @Now ) AS EndDate
INTO
    #DateRanges
FROM 
    #Numbers
WHERE
    n <= 24

/* #DateRanges now contains 24 rows that look like this:

StartDate               EndDate
2016-08-04 15:22:26.223 2016-08-04 16:22:26.223
2016-08-04 14:22:26.223 2016-08-04 15:22:26.223
2016-08-04 13:22:26.223 2016-08-04 14:22:26.223
2016-08-04 12:22:26.223 2016-08-04 13:22:26.223
...

Script was run at 2016-08-04 16:22:26.223. The first row's end date is that time. 
This table expresses 24 one-hour datetime ranges ending at the current time. 
It's also  easy to make 24 one-hour ranges for one calendar day, or anything
similar.
*/

--Now we just join that table to our #Test table to group the rows those date ranges.

SELECT
    COUNT(*) AS [Count],
    #DateRanges.StartDate
FROM
    #Test
        JOIN
    #DateRanges
        ON #Test.DateCreated >= #DateRanges.StartDate
        AND #Test.DateCreated < #DateRanges.EndDate
GROUP BY
    #DateRanges.StartDate

/*
Since we used two different getdate() calls to populate our two tables, the last record of 
our #Test table is outside of the range of our #DateRange's last row by a few milliseconds,
so we only get 23 results from this query. This script is just an illustration.
*/

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接