query - dapper mapping example
Manualmente mapear nombres de columna con propiedades de clase (13)
Soy nuevo en Dapper Micro ORM. Hasta ahora, puedo usarlo para materiales ORM relacionados pero no puedo mapear los nombres de las columnas de la base de datos con las propiedades de la clase. Por ejemplo:
Tengo la tabla de la base de datos de la siguiente manera:
Table Name: Person
person_id int
first_name varchar(50)
last_name varchar(50)
y tengo la clase llamada Persona
public class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Tenga en cuenta que los nombres de mi columna en la tabla son diferentes del nombre de la propiedad de la clase a la que estoy tratando de asignar los datos que obtuve del resultado de la consulta.
var sql = @"select top 1 PersonId,FirstName,LastName from Person";
using (var conn = ConnectionFactory.GetConnection())
{
var person = conn.Query<Person>(sql).ToList();
return person;
}
El código anterior no funcionará ya que los nombres de las columnas no coincidirán con las propiedades del objeto (Persona). En este escenario, ¿hay algo que pueda hacer en Dapper para mapear manualmente (por ejemplo, person_id => PersonId
) los nombres de las columnas con las propiedades del objeto?
Cualquier pista o ayuda sería muy apreciada.
Antes de abrir la conexión a su base de datos, ejecute este fragmento de código para cada una de sus clases poco:
// Section
SqlMapper.SetTypeMap(typeof(Section), new CustomPropertyTypeMap(
typeof(Section), (type, columnName) => type.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(false).OfType<ColumnAttribute>().Any(attr => attr.Name == columnName))));
A continuación, agregue las anotaciones de datos a sus clases poco como esta:
public class Section
{
[Column("db_column_name1")] // Side note: if you create aliases, then they would match this.
public int Id { get; set; }
[Column("db_column_name2")]
public string Title { get; set; }
}
Después de eso, ya está todo listo. Simplemente haga una llamada a la consulta, algo así como:
using (var sqlConnection = new SqlConnection("your_connection_string"))
{
var sqlStatement = "SELECT " +
"db_column_name1, " +
"db_column_name2 " +
"FROM your_table";
return sqlConnection.Query<Section>(sqlStatement).AsList();
}
Aquí hay una solución simple que no requiere atributos que le permitan mantener el código de infraestructura fuera de sus POCO.
Esta es una clase para tratar con las asignaciones. Un diccionario funcionaría si asignaras todas las columnas, pero esta clase te permite especificar solo las diferencias. Además, incluye mapas inversos para que pueda obtener el campo de la columna y la columna del campo, lo que puede ser útil al hacer cosas como generar sentencias SQL.
public class ColumnMap
{
private readonly Dictionary<string, string> forward = new Dictionary<string, string>();
private readonly Dictionary<string, string> reverse = new Dictionary<string, string>();
public void Add(string t1, string t2)
{
forward.Add(t1, t2);
reverse.Add(t2, t1);
}
public string this[string index]
{
get
{
// Check for a custom column map.
if (forward.ContainsKey(index))
return forward[index];
if (reverse.ContainsKey(index))
return reverse[index];
// If no custom mapping exists, return the value passed in.
return index;
}
}
}
Configure el objeto ColumnMap y diga a Dapper que use la asignación.
var columnMap = new ColumnMap();
columnMap.Add("Field1", "Column1");
columnMap.Add("Field2", "Column2");
columnMap.Add("Field3", "Column3");
SqlMapper.SetTypeMap(typeof (MyClass), new CustomPropertyTypeMap(typeof (MyClass), (type, columnName) => type.GetProperty(columnMap[columnName])));
Dapper ahora admite columnas personalizadas para mapeadores de propiedades. Lo hace a través de la interfaz ITypeMap . CustomPropertyTypeMap proporciona una clase CustomPropertyTypeMap que puede hacer la mayor parte de este trabajo. Por ejemplo:
Dapper.SqlMapper.SetTypeMap(
typeof(TModel),
new CustomPropertyTypeMap(
typeof(TModel),
(type, columnName) =>
type.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(false)
.OfType<ColumnAttribute>()
.Any(attr => attr.Name == columnName))));
Y el modelo:
public class TModel {
[Column(Name="my_property")]
public int MyProperty { get; set; }
}
Es importante tener en cuenta que la implementación de CustomPropertyTypeMap requiere que el atributo exista y coincida con uno de los nombres de columna o la propiedad no se correlacionará. La clase DefaultTypeMap proporciona la funcionalidad estándar y se puede aprovechar para cambiar este comportamiento:
public class FallbackTypeMapper : SqlMapper.ITypeMap
{
private readonly IEnumerable<SqlMapper.ITypeMap> _mappers;
public FallbackTypeMapper(IEnumerable<SqlMapper.ITypeMap> mappers)
{
_mappers = mappers;
}
public SqlMapper.IMemberMap GetMember(string columnName)
{
foreach (var mapper in _mappers)
{
try
{
var result = mapper.GetMember(columnName);
if (result != null)
{
return result;
}
}
catch (NotImplementedException nix)
{
// the CustomPropertyTypeMap only supports a no-args
// constructor and throws a not implemented exception.
// to work around that, catch and ignore.
}
}
return null;
}
// implement other interface methods similarly
// required sometime after version 1.13 of dapper
public ConstructorInfo FindExplicitConstructor()
{
return _mappers
.Select(mapper => mapper.FindExplicitConstructor())
.FirstOrDefault(result => result != null);
}
}
Y con eso en su lugar, se vuelve fácil crear un mapeador de tipos personalizado que usará los atributos automáticamente si están presentes, pero de lo contrario recurrirán al comportamiento estándar:
public class ColumnAttributeTypeMapper<T> : FallbackTypeMapper
{
public ColumnAttributeTypeMapper()
: base(new SqlMapper.ITypeMap[]
{
new CustomPropertyTypeMap(
typeof(T),
(type, columnName) =>
type.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(false)
.OfType<ColumnAttribute>()
.Any(attr => attr.Name == columnName)
)
),
new DefaultTypeMap(typeof(T))
})
{
}
}
Eso significa que ahora podemos admitir fácilmente tipos que requieren atributos de mapa usando:
Dapper.SqlMapper.SetTypeMap(
typeof(MyModel),
new ColumnAttributeTypeMapper<MyModel>());
Aquí hay un resumen del código fuente completo .
Esto es una copia de seguridad de otras respuestas. Es solo un pensamiento que tuve para administrar las cadenas de consulta.
Person.cs
public class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public static string Select()
{
return $"select top 1 person_id {nameof(PersonId)}, first_name {nameof(FirstName)}, last_name {nameof(LastName)}from Person";
}
}
Método API
using (var conn = ConnectionFactory.GetConnection())
{
var person = conn.Query<Person>(Person.Select()).ToList();
return person;
}
Esto funciona bien:
var sql = @"select top 1 person_id PersonId, first_name FirstName, last_name LastName from Person";
using (var conn = ConnectionFactory.GetConnection())
{
var person = conn.Query<Person>(sql).ToList();
return person;
}
Dapper no tiene ninguna función que le permita especificar un atributo de columna , no estoy en contra de agregar soporte para ello, siempre que no logremos la dependencia.
Hago lo siguiente usando dynamic y LINQ:
var sql = @"select top 1 person_id, first_name, last_name from Person";
using (var conn = ConnectionFactory.GetConnection())
{
List<Person> person = conn.Query<dynamic>(sql)
.Select(item => new Person()
{
PersonId = item.person_id,
FirstName = item.first_name,
LastName = item.last_name
}
.ToList();
return person;
}
La solución de Kaleb Pederson funcionó para mí. Actualicé ColumnAttributeTypeMapper para permitir un atributo personalizado (tenía el requisito de dos asignaciones diferentes en el mismo objeto de dominio) y propiedades actualizadas para permitir a los instaladores privados en los casos en que era necesario derivar un campo y diferir los tipos.
public class ColumnAttributeTypeMapper<T,A> : FallbackTypeMapper where A : ColumnAttribute
{
public ColumnAttributeTypeMapper()
: base(new SqlMapper.ITypeMap[]
{
new CustomPropertyTypeMap(
typeof(T),
(type, columnName) =>
type.GetProperties( BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(prop =>
prop.GetCustomAttributes(true)
.OfType<A>()
.Any(attr => attr.Name == columnName)
)
),
new DefaultTypeMap(typeof(T))
})
{
//
}
}
Messing con el mapeo está en el límite de pasar a tierra real ORM. En lugar de luchar con él y mantener a Dapper en su verdadera forma simple (rápida), simplemente modifique su SQL ligeramente así:
var sql = @"select top 1 person_id as PersonId,FirstName,LastName from Person";
Por algún tiempo, lo siguiente debería funcionar:
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
Si usa .NET 4.5.1 o posterior, Dapper.FluentColumnMapping de Dapper.FluentColumnMapping para mapear el estilo LINQ. Le permite separar completamente la asignación de db de su modelo (sin necesidad de anotaciones)
Tomado de las pruebas Dapper que actualmente se encuentra en Dapper 1.42.
// custom mapping
var map = new CustomPropertyTypeMap(typeof(TypeWithMapping),
(type, columnName) => type.GetProperties().FirstOrDefault(prop => GetDescriptionFromAttribute(prop) == columnName));
Dapper.SqlMapper.SetTypeMap(typeof(TypeWithMapping), map);
Clase de ayuda para quitar el nombre del atributo Descripción (Yo personalmente he usado Columna como ejemplo de @kalebs)
static string GetDescriptionFromAttribute(MemberInfo member)
{
if (member == null) return null;
var attrib = (DescriptionAttribute)Attribute.GetCustomAttribute(member, typeof(DescriptionAttribute), false);
return attrib == null ? null : attrib.Description;
}
Clase
public class TypeWithMapping
{
[Description("B")]
public string A { get; set; }
[Description("A")]
public string B { get; set; }
}
Una manera fácil de lograr esto es simplemente usar alias en las columnas de su consulta. Si la columna de su base de datos es PERSON_ID
y la PERSON_ID
su objeto es ID
, puede select PERSON_ID as Id ...
en su consulta y Dapper la levantará como se esperaba.
para todos los que usan Dapper 1.12, esto es lo que deben hacer para hacer esto:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property]
public class ColumnAttribute : Attribute
{
public string Name { get; set; }
public ColumnAttribute(string name)
{
this.Name = name;
}
}
map = new DefaultTypeMap(type);
y comentarlo.
map = new CustomPropertyTypeMap(type, (t, columnName) =>
{
PropertyInfo pi = t.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(false)
.OfType<ColumnAttribute>()
.Any(attr => attr.Name == columnName));
return pi != null ? pi : t.GetProperties().FirstOrDefault(prop => prop.Name == columnName);
});