Npgsql支持分别从macaddr和inet类型的查询结果集中解析System.Net.NetworkInformation.PhysicalAddress
和System.Net.IPAddress
。例如,可以使用带有Dapper的Npgsql填充以下类:
-- 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以二进制形式发送数据,我认为这意味着类型inet和macaddr有一些二进制表示。但是,当我使用上面相同的声明运行以下代码时...
// 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);
我得到了例外:
查询问题:SELECT * FROM foo WHERE macaddress =:macAddress
System.NotSupportedException:System.Net.NetworkInformation.PhysicalAddress类型的成员macAddress不能用作参数值
Dapper / Npgsql如何分别知道如何从inet和macaddr类型的列中解析IPAddress
和PhysicalAddress
,但我无法将这些类型用作参数?在以前的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);
生成异常:
查询问题:SELECT * FROM foo WHERE macaddress =:macAddress
Npgsql.NpgsqlException:42883:运算符不存在: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:运算符不存在:bar =整数
相反,我将查询更改为:
SELECT * FROM log WHERE mybar = :barParam::bar
然后我得到了例外:
42846:无法将类型整数转换为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);
当然有更好的方法来解决这个问题。任何人都可以对这是什么有所了解吗?
感谢Hambone和Shay的帮助,我找到了解决IPAddress
和PhysicalAddress
类型的方法。问题是inet
和macaddr
是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)类的静态构造函数中,我只需添加以下行:
var ipAddressHandler =
new PassThroughHandler<IPAddress>(NpgsqlDbType.Inet);
var macAddressHandler =
new PassThroughHandler<PhysicalAddress>(NpgsqlDbType.MacAddr);
SqlMapper.AddTypeHandler(ipAddressHandler);
SqlMapper.AddTypeHandler(macAddressHandler);
现在我可以通过Dapper发送PhysicalAddress
和IPAddress
参数,而无需对它们进行字符串化。
然而,Enum提出了另一项挑战,因为Dapper 1.42不支持添加自定义枚举处理程序(参见Dapper问题#259 / #286 )。更不幸的是,Dapper默认将枚举值作为整数发送到底层实现。因此, 当使用Dapper 1.42(或更早版本)时 , 目前无法将枚举值发送到Npgsql而不将它们转换为字符串 。我已经联系过Marc Gravell关于这个问题,并希望在不久的将来得到某种解决方案。在那之前,决议是:
1)直接使用Npgsql,绕过Dapper
2)将所有枚举值作为文本发送,并转换为查询中的相应类型
我个人选择继续选择#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传递枚举值参数 。
在参数处理方面,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类型。看看我们在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);
这里的问题是你提供了一个需要PhysicalAddress的字符串,这就是PostgreSQL抱怨你将macaddr类型与文本类型进行比较的原因。
关于枚举,Npgsql 3.0.0包括直接编写和读取枚举的支持,而无需通过字符串表示。但是,您需要事先通过调用NpgsqlConnection.RegisterEnumGlobally(“pg_enum_type_name”)让Npgsql知道您的枚举类型。不幸的是,我还没有记录新的枚举支持,这很快就会发生。