Dapper IPAddress/PhysicalAddress/Enum Npgsql v3 的参数支持

Dapper IPAddress/PhysicalAddress/Enum Parameter Support Over Npgsql v3

npgsql支持从macaddrinet类型的查询结果集中分别解析System.Net.NetworkInformation.PhysicalAddressSystem.Net.IPAddress .例如,可以使用 Npgsql 和 Dapper 填充以下 class:

-- Postgres CREATE TABLE command
CREATE TABLE foo (
    ipaddress inet,
    macaddress macaddr
);
// C# class for type "foo"
public class foo
{
    public IPAddress ipaddress { get; set; }
    public PhysicalAddress macaddress { get; set; }
}

// Code that loads all data from table "foo"
IDbConnection connection = new NpgsqlConnection(connectionString);
var foos = connection.Query<foo>("SELECT * FROM foo");

由于 Npgsql v3.0.1 以二进制形式发送数据,我假设这意味着类型 inetmacaddr 有一些二进制表示.但是,当我 运行 下面的代码使用上面相同的声明时...

// Code that tries to load a specific row from "foo"
var query = "SELECT * FROM foo WHERE macaddress = :macAddress";
var queryParams = new DynamicParameters();
queryParams.Add("macAddress", PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF"));
IDbConnection connection = new NpgsqlConnection(connectionString);
var foos = connection.Query<foo>(query, queryParams);

我得到异常:

Problem with query: SELECT * FROM foo WHERE macaddress = :macAddress
System.NotSupportedException : The member macAddress of type System.Net.NetworkInformation.PhysicalAddress cannot be used as a parameter value

Dapper/Npgsql 怎么知道如何从类型为 inet 和 [=54 的列中解析 IPAddressPhysicalAddress =]macaddr,但是我无法使用这些类型作为参数?在之前的Npgsql版本中,我只是简单的将ToString()结果作为参数值发送出去,但是在Npgsql v3.0.1中下面的代码...

// Code that tries to load a specific row from "foo"
// The only change from above is the "ToString()" method called on PhysicalAddress
var query = "SELECT * FROM foo WHERE macaddress = :macAddress";
var queryParams = new DynamicParameters();
queryParams.Add("macAddress", PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF").ToString());
IDbConnection connection = new NpgsqlConnection(connectionString);
var foos = connection.Query<foo>(query, queryParams);

生成异常:

Problem with query: SELECT * FROM foo WHERE macaddress = :macAddress
Npgsql.NpgsqlException : 42883: operator does not exist: macaddr = text

我知道我可以将查询更改为 "SELECT * FROM foo WHERE macaddress = :macAddress::macaddr",但我想知道是否有更简洁的方法来解决这个问题?有没有计划在不久的将来增加对这些类型的支持?

-- 开始编辑--

我刚刚意识到同样的问题困扰着枚举类型。如果我有一个枚举参数,我可以从查询结果中解析它,但无法将枚举传递给 Postgres。例如:

CREATE TYPE bar AS ENUM (
    val1,
    val2
);

CREATE TABLE log (
    mybar bar
);
public enum bar
{
    val1,
    val2
}

public class log
{
    public bar mybar { get; set; }
}

// Code that loads all data from table "log"
NpgsqlConnection.RegisterEnumGlobally<bar>();
IDbConnection connection = new NpgsqlConnection(connectionString);
var logs = connection.Query<log>("SELECT * FROM log");

// Code that attempts to get rows from log with a specific enum
var query = "SELECT * FROM log WHERE mybar = :barParam";
var queryParams = new DynamicParameters();
queryParams.Add("barParam", bar.val1);
// The following throws an exception
logs = connection.Query<log>(query, queryParams);

在上面,一切正常,直到抛出以下异常的最后一行:

42883: operator does not exist: bar = integer

如果相反,我将查询更改为:

SELECT * FROM log WHERE mybar = :barParam::bar

然后我得到异常:

42846: cannot cast type integer to bar

我可以将枚举值作为参数传递的唯一方法是将它们作为文本传递并在查询中转换参数,如下所示:

// Code that successfully performs the query
var query = "SELECT * FROM log WHERE mybar = :barParam::bar";
var queryParams = new DynamicParameters();
queryParams.Add("barParam", bar.val1.ToString());
logs = connection.Query<log>(query, queryParams);

当然有更好的方法来解决这个问题。谁能解释一下那是什么?

我不得不承认,我不熟悉 DynamicParameters class...使用本机 NpgSql 库,但是,我认为您可以完成上面列出的基本任务。

这是一个显式参数声明:

List<foo> foos = new List<AdLookup.foo>();

NpgsqlConnection conn = new NpgsqlConnection(ConnectionString);
conn.Open();

NpgsqlCommand cmd = new NpgsqlCommand(
    "select * from foo where macaddress = :macAddress", conn);
cmd.Parameters.Add("macAddress", NpgsqlTypes.NpgsqlDbType.MacAddr);
cmd.Parameters[0].Value = PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF");

NpgsqlDataReader reader = cmd.ExecuteReader();

while (reader.Read())
{
    foo f = new foo();
    f.ipaddress = (IPAddress)reader.GetValue(0);
    f.macaddress = (PhysicalAddress)reader.GetValue(1);
    foos.Add(f);
}

reader.Close();
conn.Close();

当您创建参数时,您使用参数的值作为第二个参数。同样,我不熟悉 DynamicParameter class,但在 NpgSqlParameter class 中,您可以使用 AddWithValue 方法完成此操作:

cmd.Parameters.AddWithValue("macAddress", PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF"));

这可以替换上面的 cmd.Parameters.Addcmd.Parameters[0] 行。

同样,我不知道这是否有帮助...但我确实想解决一种将参数发送到查询的方法。

如果DynamicParameters支持AddWithValue,您的解决方案可能就这么简单。

在参数处理方面,Npgsql 3.0 的行为与以前的版本有些不同,并且在许多情况下更加严格。在上面的示例中,区分与 Dapper 相关的问题(与 Npgsql 无关)和 Npgsql 问题很重要。

简而言之,Npgsql 可以将 PhysicalAddress 实例转换为 macaddr 的 PostgreSQL 二进制表示,反之亦然。与以前的版本不同,它将不再透明地接受文本表示,由您来解析它们并提供一个 PhysicalAddress 实例。

var query = "SELECT * FROM foo WHERE macaddress = :macAddress";
var queryParams = new DynamicParameters();
queryParams.Add("macAddress", PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF"));
IDbConnection connection = new NpgsqlConnection(connectionString);
var foos = connection.Query<foo>(query, queryParams);

这里的问题可能是 Dapper 不知道 PhysicalAddress 类型。查看 this issue 我们在 3.0.0 中包含了一个用于 jsonb 的 Dapper 类型处理程序,您必须对 PhysicalAddress 执行相同的操作。

// Code that tries to load a specific row from "foo"
// The only change from above is the "ToString()" method called on PhysicalAddress
var query = "SELECT * FROM foo WHERE macaddress = :macAddress";
var queryParams = new DynamicParameters();
queryParams.Add("macAddress", PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF").ToString());
IDbConnection connection = new NpgsqlConnection(connectionString);
var foos = connection.Query<foo>(query, queryParams);

这里的问题是您在需要物理地址的地方提供了一个字符串,这就是为什么 PostgreSQL 抱怨您将 macaddr 类型与文本类型进行比较。

关于枚举,Npgsql 3.0.0 支持直接写入和读取枚举,而无需通过字符串表示形式。但是,您确实需要通过提前调用 NpgsqlConnection.RegisterEnumGlobally("pg_enum_type_name") 让 Npgsql 提前知道您的枚举类型。不幸的是,我还没有开始记录新的枚举支持,这很快就会发生。

感谢 Hambone 和 Shay 的帮助,我找到了解决 IPAddressPhysicalAddress 类型的方法。问题是 inetmacaddr 是特定于 Postgres 的,而 Dapper 似乎与提供者无关。因此,解决方案是添加一个自定义处理程序,在将这些参数类型转发到 Npgsql 之前设置适当的 NpgsqlDbType。自定义处理程序可以编码为:

using System;
using System.Data;
using Dapper;
using Npgsql;
using NpgsqlTypes;

namespace MyNamespace
{
    internal class PassThroughHandler<T> : SqlMapper.TypeHandler<T>
    {

        #region Fields

        /// <summary>Npgsql database type being handled</summary>
        private readonly NpgsqlDbType _dbType;

        #endregion

        #region Constructors

        /// <summary>Constructor</summary>
        /// <param name="dbType">Npgsql database type being handled</param>
        public PassThroughHandler(NpgsqlDbType dbType)
        {
            _dbType = dbType;
        }

        #endregion

        #region Methods

        public override void SetValue(IDbDataParameter parameter, T value)
        {
            parameter.Value = value;
            parameter.DbType = DbType.Object;
            var npgsqlParam = parameter as NpgsqlParameter;
            if (npgsqlParam != null)
            {
                npgsqlParam.NpgsqlDbType = _dbType;
            }
        }

        public override T Parse(object value)
        {
            if (value == null || value == DBNull.Value)
            {
                return default(T);
            }
            if (!(value is T))
            {
                throw new ArgumentException(string.Format(
                    "Unable to convert {0} to {1}",
                    value.GetType().FullName, typeof(T).FullName), "value");
            }
            var result = (T)value;
            return result;
        }

        #endregion

    }
}

然后,在我的数据访问层 (DAL) class 的静态构造函数中,我只需添加以下行:

var ipAddressHandler  =
    new PassThroughHandler<IPAddress>(NpgsqlDbType.Inet);
var macAddressHandler =
    new PassThroughHandler<PhysicalAddress>(NpgsqlDbType.MacAddr);
SqlMapper.AddTypeHandler(ipAddressHandler);
SqlMapper.AddTypeHandler(macAddressHandler);

现在我可以通过 Dapper 发送 PhysicalAddressIPAddress 参数,而不需要将它们字符串化。

然而,枚举带来了另一个挑战,因为 Dapper 1.42 不支持添加自定义枚举处理程序(请参阅 Dapper 问题 #259/#286)。更不幸的是,Dapper 默认情况下将枚举值作为整数发送到底层实现。因此,目前无法在使用 Dapper 1.42(或更早版本) 时将枚举值发送到 Npgsql 而不将它们转换为字符串。我已经就此问题联系了 Marc Gravell,并希望在不久的将来得到某种解决方案。在那之前,分辨率是:

1) Use Npgsql directly, bypassing Dapper
2) Send all enum values as text, and cast to the appropriate type in the query

我个人选择继续选项 #2。


开始编辑

在查看了 Dapper 源代码后,我意识到还有第三种选择可以实现此目的。虽然不可能为每个枚举类型创建自定义处理程序,但可以将枚举值包装在 SqlMapper.ICustomQueryParameter 对象中。由于代码只需要将枚举值传递给Npgsql,所以实现很简单:

using System;
using System.Data;
using Dapper;

namespace MyNamespace
{
    internal class EnumParameter : SqlMapper.ICustomQueryParameter
    {

        #region Fields

        /// <summary>Enumerated parameter value</summary>
        private readonly Enum _val;

        #endregion

        #region Constructors

        /// <summary>Constructor</summary>
        /// <param name="val">Enumerated parameter value</param>
        public EnumParameter(Enum val)
        {
            _val = val;
        }

        #endregion

        #region Methods

        public void AddParameter(IDbCommand command, string name)
        {
            var param = command.CreateParameter();
            param.ParameterName = name;
            param.DbType = DbType.Object;
            param.Value = _val;
            command.Parameters.Add(param);
        }

        #endregion

    }
}

我的代码已经设置好,每个参数都添加到 Dictionary<string, object>,然后在单个代码路径中转换为 DynamicParameters 对象。因此,我能够将以下检查添加到从一个转换为另一个的循环中:

var queryParams = new DynamicParameters();
foreach (var kvp in paramDict)
{
    var enumParam = kvp.Value as Enum;
    if (enumParam == null)
    {
        queryParams.Add(kvp.key, kvp.Value);
    }
    else
    {
        queryParams.Add(kvp.key, new EnumParameter(enumParam));
    }
}

通过这样做,枚举值被传递给 Npgsql 而没有被转换为它们的等效数字(因此,没有丢失相关的类型信息)。整个过程看起来仍然非常复杂,但至少 有一种方法可以使用 Npgsql v3 二进制形式通过 Dapper 传递枚举值参数