简体   繁体   English

Dapper 可以将用户定义的复合类型传递给 PostgreSQL 函数吗?

[英]Can Dapper pass a user-defined composite type to a PostgreSQL function?

I'm trying to figure out how to use Dapper to pass in a user-defined composite type to a PostgreSQL function.我试图弄清楚如何使用 Dapper 将用户定义的复合类型传递给 PostgreSQL 函数。 I know this is possible with SQL Server and I've already got working examples using Dapper+SQL Server, however, I'm coming up short on how to do the same with PostgreSQL.我知道这在 SQL Server 上是可能的,而且我已经有了使用 Dapper+SQL Server 的工作示例,但是,我对如何使用 PostgreSQL 做同样的事情很不了解。

From some of the things I've read, I'm not sure if this is even possible with Dapper+PostgreSQL, but I know that it does work with plain Npgsql (and I've got a working example of that as well).从我读过的一些内容来看,我不确定 Dapper+PostgreSQL 是否可行,但我知道它确实适用于普通的 Npgsql(我也有一个工作示例)。

So, how does one call a PostgreSQL function that takes in a user-defined composite type using Dapper?那么,如何使用 Dapper 调用接受用户定义复合类型的 PostgreSQL 函数?

Example user-defined composite type示例用户定义的复合类型

CREATE TYPE hero AS (
    first_name text,
    last_name text
);

Example PostgreSQL function that takes in the user-defined composite type接受用户定义的复合类型的示例 PostgreSQL 函数

CREATE OR REPLACE FUNCTION testfuncthattakesinudt(our_hero hero)
    RETURNS SETOF characters 
    LANGUAGE 'sql'

    STABLE
    ROWS 1000
AS $BODY$

    SELECT  *
    FROM    characters
    WHERE   first_name = COALESCE(our_hero.first_name, '')
    AND     last_name = COALESCE(our_hero.last_name, '');

$BODY$;

Theoretic C# Example理论 C# 示例

[Test]
public void UsingDapper_Query_CallFunctionThatTakesInUserDefinedCompositeType_FunctionUsesUserDefinedCompositeType()
{
    // Arrange
    using (var conn = new NpgsqlConnection(_getConnectionStringToDatabase()))
    {
        var funcName = "testfuncthattakesinudt";
        var expect = CharacterTestData.First();

        // Act
        var result = conn.Query<Character>(funcName,
            new
            {
                our_hero = new
                {
                    first_name = CharacterTestData.First().first_name,
                    last_name = CharacterTestData.First().last_name
                }
            },
            commandType: CommandType.StoredProcedure
        );

        // Assert
        Assert.AreEqual(expect, result);
    }
}

I know that using plain Npgsql, it would be necessary to create the parameter something similar to:我知道使用普通的 Npgsql,有必要创建类似于以下内容的参数:

var udtCompositeParameter = new NpgsqlParameter
{
    ParameterName = "our_hero",
    Value = new
    {
        first_name = CharacterTestData.First().first_name,
        last_name = CharacterTestData.First().last_name
    },
    DataTypeName = "hero"
};

But using Dapper, I haven't found a way to set DataTypeName or something similar.但是使用 Dapper,我还没有找到设置DataTypeName或类似内容的方法。 I've tried many different ways to shape the parameter for Dapper (for example using something like DynamicParameter and specifying dbType: DbType.Object ), but regardless, I always get some similar error related to the composite type.我尝试了许多不同的方式来塑造精致小巧的参数(例如,使用类似DynamicParameter并指定dbType: DbType.Object ),但无论如何,我总是得到相关的复合型的一些类似的错误。 I've also looked at the Dapper source , but from what I saw, it was light on PostgreSQL specific tests and those tests that seemed to be inline with what I'm trying to do were tailored to SQL Server.我还查看了Dapper 源代码,但从我所见,PostgreSQL 特定测试和那些似乎与我正在尝试做的测试一致的测试是针对 SQL Server 量身定制的。

UPDATE更新

Based on this answer , we were able to apply that concept to this question and provide a much cleaner result that didn't require the use of TypeHandler s.基于这个答案,我们能够将这个概念应用于这个问题,并提供一个更清晰的结果,不需要使用TypeHandler s。

Working Integration Test工作集成测试

public void Query_CallFunctionThatTakesInUserDefinedType_FunctionUsesUserDefinedType()
{
    // Arrange
    using (var conn = Db.GetConnection())
    {
        var functionName = "test_function_that_takes_in_udt";
        var expect = CharacterTestData.First();

        // Act
        var result = conn.Query<Character>(functionName,
            new 
            {
                our_hero = new HeroParameter(
                    CharacterTestData.First().FirstName,
                    CharacterTestData.First().LastName
                )
            },
            commandType: CommandType.StoredProcedure
        ).FirstOrDefault();

        // Assert
        Assert.AreEqual(expect, result);
    }
}

NOTE There are some refactorings (names for example) in the code above since the original question, however, the body of the PostgreSQL function was unchanged.注意自从原始问题以来,上面的代码中有一些重构(例如名称),但是,PostgreSQL 函数的主体没有改变。 The most notable refactoring related to this answer is how the connection is created:与此答案相关的最显着重构是如何创建连接:

public NpgsqlConnection GetConnection()
{
    var connection = new NpgsqlConnection(GetConnectionStringToDatabase());
    Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
    connection.Open();
    connection.ReloadTypes();

    /*
     * 
     * Ideally, should move this to be handled for _all_ connections like:
     *      NpgsqlConnection.GlobalTypeMapper.MapEnum<SomeEnum>("some_enum_type");
     *      NpgsqlConnection.GlobalTypeMapper.MapComposite<SomeType>("some_composite_type");
     *  
     */
    connection.TypeMapper.MapComposite<Hero>("hero");

    return connection;
}

Supporting HeroParameter支持英雄参数

public class HeroParameter : ICustomQueryParameter
{
    private readonly Hero _hero;

    public HeroParameter(string firstName, string lastName)
    {
        _hero = new Hero(firstName, lastName);
    }

    public void AddParameter(IDbCommand command, string name)
    {
        var parameter = new NpgsqlParameter
        {
            ParameterName = name,
            Value = _hero,
            DataTypeName = "hero"
        };
        command.Parameters.Add(parameter);
    }
}

Not using TypeHandlers proved to be very advantageous in our situation.在我们的情况下,不使用TypeHandlers被证明是非常有利的。

This is due to the finicky nature in which PostgreSQL might use a UDT/composite type when being returned from a function.这是由于 PostgreSQL 在从函数返回时可能使用 UDT/复合类型的挑剔性质。 For example, if the UDT is one of 2 or more columns being returned, PostgreSQL returns the result set with the UDT column in the shape of (val1, val2) .例如,如果 UDT 是要返回的 2 个或更多列之一,则 PostgreSQL 将返回带有(val1, val2)形状的 UDT 列的结果集。 However, if the thing being returned is just the UDT, PostgreSQL will expand the UDT's individual properties to individual columns, much like a normal SELECT from a table.但是,如果返回的只是UDT,PostgreSQL 会将 UDT 的各个属性扩展到各个列,就像从表中进行普通SELECT一样。

For example, consider the following function:例如,考虑以下函数:

CREATE OR REPLACE FUNCTION example_function()
    RETURNS hero
    LANGUAGE SQL
AS
$$
SELECT ('Peter', 'Parker')::hero as heroes
$$

In this scenario, in order for the TypeHandler to work, we need the result to be in the following format:在这种情况下,为了让TypeHandler工作,我们需要将结果采用以下格式:

|-----------------|
|      heroes     |
|-----------------|
| (Peter, Parker) |
|-----------------|

This is because after a call like conn.Query<Hero>(...) , Dapper will pass the first column (and only the first) to the TypeHandler expecting it to do the necessary conversion.这是因为在调用conn.Query<Hero>(...) ,Dapper 会将第一列(并且只有第一列)传递给TypeHandler期望它进行必要的转换。

However, the output from the above function, example_function , will actually return the result in the following expanded format:但是,上述函数example_function的输出实际上将以以下扩展格式返回结果:

|------------|-----------|
| first_name | last_name |
|------------|-----------|
|   Peter    |  Parker   |
|------------|-----------|

This means that the Type of value that gets passed to the TypeHandler.Parse() method is string in this example.这意味着该Typevalue获取传递到TypeHandler.Parse()方法是string在这个例子。

However, for functions that return the UDT as one of the columns when 2 or more columns are returned, then the TypeHandler works as expected because the single column's value is passed to the Parse method.但是,对于在返回 2 个或更多列时将 UDT 作为列之一返回的函数, TypeHandler按预期工作,因为将单个列的值传递给Parse方法。

Consider this updated function:考虑这个更新的函数:

CREATE OR REPLACE FUNCTION example_function()
    RETURNS TABLE (year integer, hero hero)
    LANGUAGE SQL
AS
$$
SELECT 1962 AS year, ('Peter', 'Parker')::hero AS hero
$$

Which returns the output in the following format:它以以下格式返回输出:

|------|-----------------|
| year |      hero       |
|------|-----------------|
| 1962 | (Peter, Parker) |
|------|-----------------|

This is where the original solution below falls short.这是下面的原始解决方案不足的地方。 In that example, I wasn't (yet) using the Parse method.在那个例子中,我(还)没有使用Parse方法。 However, once I needed to implement that function to support UDTs being returned , the TypeHandler wouldn't work based on the ways PostgreSQL returns the UDTs as demonstrated above.但是,一旦我需要实现正返回功能,支持UDT,该TypeHandler是行不通的基于上面所证明的PostgreSQL返回的UDT的方式。


ORIGINAL ANSWER原答案

For anyone else that might stumble upon this question, this worked for me, although I'm not terribly happy with it, so I'm open to better solutions to this problem.对于可能偶然发现这个问题的任何其他人,这对我有用,尽管我对此并不十分满意,所以我愿意为这个问题提供更好的解决方案。

Working Integration Test工作集成测试

[Test]
public void Query_CallFunctionThatTakesInUserDefinedType_FunctionUsesUserDefinedType()
{
    // Arrange
    using (var conn = new NpgsqlConnection(Db.GetConnectionStringToDatabase()))
    {
        var funcName = "testfuncthattakesinudt";
        var expect = CharacterTestData.First();

        SqlMapper.AddTypeHandler(new HeroTypeHandler());
        conn.Open();
        conn.ReloadTypes();
        conn.TypeMapper.MapComposite<Hero>("hero");

        // Act
        var result = conn.Query<Character>(funcName,
            new
            {
                our_hero = new Hero
                {
                    first_name = CharacterTestData.First().first_name,
                    last_name = CharacterTestData.First().last_name
                }
            },
            commandType: CommandType.StoredProcedure
        ).FirstOrDefault();

        // Assert
        Assert.AreEqual(expect, result);
    }
}

Supporting HeroTypeHandler支持 HeroTypeHandler

internal class HeroTypeHandler : SqlMapper.TypeHandler<Hero>
{
    public override Hero Parse(object value)
    {
        throw new NotImplementedException();
    }

    public override void SetValue(IDbDataParameter parameter, Hero value)
    {
        parameter.Value = value;
    }
}

The fix seemed to be two parts:修复似乎分为两部分:

  1. It was necessary to add a HeroTypeHandler and map it via SqlMapper.AddTypeHandler .有必要添加一个HeroTypeHandler并通过SqlMapper.AddTypeHandler映射它。
  2. I needed to map my type Hero to my PostgreSQL composite type hero via conn.TypeMapper.MapComposite .我需要通过conn.TypeMapper.MapComposite将我的类型Hero映射到我的 PostgreSQL 复合类型hero However, my gut feeling (so far) is that this is just me fighting my own integration tests, as in a real application it would probably (?) be ideal to register all composite types globally at the start of the application.然而,我的直觉(到目前为止)是这只是我在与我自己的集成测试作斗争,因为在实际应用程序中,在应用程序开始时全局注册所有复合类型可能(?)是理想的。 (Or maybe not due to performance reasons if there is a lot of them?) (或者可能不是由于性能原因,如果有很多?)

What I don't like about this solution though is that my HeroTypeHandler doesn't really provide any real value (no pun intended).我不喜欢这个解决方案的是我的HeroTypeHandler并没有真正提供任何真正的价值(没有双关语)。 By simply assigning value to parameter.Value things worked, which my guess would have been that this is something Dapper would have done for the call, but obviously not.通过简单的分配value ,以parameter.Value工作的事情,这我的猜测一直认为这是一件小巧精致的会做的电话,但显然不是。 (?) I'd prefer to not need to do that for a lot of composite types if I had a lot of them. (?) 如果我有很多复合类型,我宁愿不需要为很多复合类型这样做。

Note that since I'm only concerned with sending this type to a PostgreSQL function, I didn't find it necessary to implement the Parse method, hence the NotImplementedException .请注意,由于我只关心将此类型发送PostgreSQL 函数,因此我认为没有必要实现Parse方法,因此是NotImplementedException YMMV青年会

Also, due to some refactoring since I posted the original question, there are some other minor differences.此外,由于我发布原始问题后进行了一些重构,因此还有一些其他细微差别。 However, they were not related to the overall fix detailed above.但是,它们与上面详述的整体修复无关。

Instead of SqlMapper.TypeHandler<Hero> or ICustomQueryParameter而不是SqlMapper.TypeHandler<Hero>ICustomQueryParameter

you can use您可以使用

Dapper.SqlMapper.AddTypeMap(typeof(Hero), DbType.Object);

I went from a simpler solution in my application:我在我的应用程序中使用了一个更简单的解决方案:

Set in the Start of my DbContext or Application the UDT Mapping.在我的 DbContext 或应用程序的开始部分设置 UDT 映射。

NpgsqlConnection.GlobalTypeMapper.MapComposite<MyContactParameter>("udt_my_contact");

MyContactParameter Class: MyContactParameter 类:

public class MyContactParameter
{
    public Guid id { get; set; }

    public string name { get; set; }

    public string company_name  { get; set; }

    public bool is_something { get; set; }
}

Calling the DB procedure using Dapper passing an array of that UDT Type使用传递该 UDT 类型的数组的 Dapper 调用 DB 过程

await dapperConnection.ExecuteAsync(procedureName, new
          {
              param_first = "string anything",
              param_udt_list = new List<MyContactParameter>{ 
                  new MyContactParameter { 
                      id = Guid.NewId(),
                      name = "any name",
                      company_name = "any company name",
                      is_something = true
                  },
                  new MyContactParameter { 
                      id = Guid.NewId(),
                      name = "any name 2",
                      company_name = "any company name 2",
                      is_something = false
                  }
              }
          }, 
          commandType: CommandType.StoredProcedure
      );

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM