Dapper with Mapping by code: Multi-Mapping with repeating column names
Asked Answered
P

3

7

I'm trying to perform a simple query and the result data is almost all null.

I have this table structure

Table Registros

ID            |  Autonumeric
TareaM_Id     |  Numeric
Fecha         |  Date/Time

and Macro_tareas table

ID            |  Autonumeric
Nombre        |  Short Text

I have mapped the classes in C# like this:

[Table("Registros")]
public class Registro
{
    [Column("ID")]
    public virtual int ID { get; set; }

    [Column("Fecha")]
    public virtual DateTime Fecha { get; set; }

    [Column("TareaM_Id")]
    public virtual int TareaM_Id { get; set; }

    public virtual MacroTarea MacroT { get; set; }
}

[Table("Macro_tarea")]
public class MacroTarea
{
    [Column("ID")]
    public virtual int ID { get; set; }

    [Column("Nombre")]
    public virtual string Nombre{ get; set; }

    public virtual ICollection<Registro> Registros { get; set; }

}

This is the query i'm trying to use

string sql = @"SELECT reg.ID, mac.ID 
    FROM Registros as reg INNER JOIN Macro_tarea as mac on reg.TareaM_Id = mac.ID
    WHERE Fecha = @Fecha";

using (IDbConnection db = new OleDbConnection(ConnectionString))
{
    var result = db.Query<Registro,MacroTarea, Registro>(sql, 
    (reg,mac) =>
    {
        reg.MacroTarea = mac;
        return reg;
    }
    ,new { @Fecha = new DateTime(2019, 1, 4).Date }
    , splitOn: "mac.ID")
    .AsList();                                            
}

I'm trying to only retrieve ids, but both id become null why is this happening?

The thing is, if I add Registros.Fecha and Macro_tarea.Nombre to the query, it got the value correctly. But id keep coming null.

Apparently the issue is happening only with ids. I suspect this issue is due to duplicate column names.

I'm working with Microsoft Access just in cast that matters.

My question is not similar to the possible duplicate because I have the classes defined as they should be mapped.

Puppis answered 7/1, 2019 at 12:40 Comment(0)
P
1

As we discussed in comments, this is an issue due to duplicate column names in two tables. This is where the similar issue and solution could be found. But, it does not include "mapping by code" as you said. So it is not exact duplicate.

I suggest you change the names of ID fields in your tables to avoid colliding them. Of-course, you should also change the name of your POCO properties and mappings accordingly.

If you cannot change the column names in table, change the POCO property name, and use the column alias in SQL query to match those new property names.

I hope this helps you.

Pelotas answered 7/1, 2019 at 14:40 Comment(0)
F
6

Renaming your database columns because your code cannot cope with the data is not a good idea. In the world of separation of concerns, why should your database care? There are good database reasons to name ID columns "Id", and you may not even have the option to change them.

There's another issue with Dapper mapping that renaming columns does not get around; repeated types. If you are trying to map to more than one instance of a class Dapper gets confused, and renaming columns won't work because you will rename both instances.

Here is the solution I have come up with. It's similar to a lot of examples that use a dictionary, except:

  • it can nest to as many levels as you like
  • can cope with Dappers 7 item limit
  • can cope with duplicates of the same class
  • can be reused e.g., for Get, GetCurrent and GetAll

In this example there is an Auction that has many Lots. Each Lot may have 1 or many Items. Items might be packs of Items. The Items are from a limited catalogue and we like relational data, so a Things table contains the details on each Item, like colour, size, etc. Here we are only getting a single Lot, but getting an Auction is the same with another level on top for Auction.

Parameter 1 - The SQL to get everything in one go

Parameter 2 - A Type array of each object we'll get back. For this reason it's best to order your SELECT to group the fields into the classes

Parameter 3 - Call the method we're about to write with the SQL result

Parameter 4 - Standard parameter array for the SQL. SQL Injection is bad, m'kay?

public async Task<List<Lot>> GetAll(int auctionId)
{
    using (var connection = new SqlConnection(_appSettings.ConnectionString))
    {
        await connection.OpenAsync();
        var result = new List<Lot>();
        await connection.QueryAsync($@"
            SELECT [Lot].*, 
                [Item].[Id], 
                [Item].[LotId], 
                [Item].[Notes], 
                itemDetails.[Id],
                itemDetails.[ThingId],
                itemDetails.[Colour], 
                itemDetails.[Size], 
                [SubItem].[Id], 
                [SubItem].[ItemId], 
                [SubItem].[Notes], 
                subItemDetails.[Id],
                subItemDetails.[ThingId],
                subItemDetails.[Colour], 
                subItemDetails.[Size]
            FROM [Lot]
                INNER JOIN [Item] ON [Item].[LotId] = [Lot].[Id]
                    LEFT JOIN [Thing] AS itemDetails ON itemDetails.[Id] = [Item].[ThingId]
                LEFT JOIN [SubItem] ON [SubItem].[ItemId] = [Item].[Id]
                    LEFT JOIN [Thing] AS subItemDetails ON subItemDetails.[Id] = [SubItem].[ThingId]
            WHERE [AuctionId] = @{nameof(auctionId)}
            ORDER BY [Lot].[Id], [Item].[Id], [Expansion].[Id];",
            new Type[] {
                typeof(Lot),
                typeof(Item),
                typeof(Thing),
                typeof(Expansion),
                typeof(Thing)
            }, 
            MapResult(result),
            new
            {
                AuctionId = auctionId
            }
        );

        return result.ToList();
    }
}

private Func<object[], Lot> MapResult(List<Lot> result)
{
    return (obj) =>
    {
        Lot lot = (Lot)obj[0];
        Item item = (Item)obj[1];
        Thing itemDetails = (Thing)obj[2];
        SubItem subItem = (SubItem)obj[3];
        Thing subItemDetails = (Thing)obj[4];
        if (lot != null)
        {
            if (result.Any(a => a.Id == lot.Id))
            {
                lot = result.First(a => a.Id == lot.Id);
            }
            else
            {
                result.Add(lot);
            }
        }
        if (item != null)
        {
            if (lot.Items.Any(i => i.Id == item.Id))
            {
                item = lot.Items.First(i => i.Id == item.Id);
            }
            else
            {
                lot.Items.Add(item.FromThing(itemDetails));
            }
        }
        if (subItem != null)
        {
            if (item.SubItems.Any(e => e.Id == subItem.Id) == false)
            {
                item.SubItems.Add(subItem.FromThing(subItemDetails));
            }
        }
            return null;
    };
}

MapResult is the meat of the code. It returns a Func with two types, the Type array we defined above and the return Type, and takes a List of the top level object. I then map each item from the object array to another of it's actual type. This keeps the code easier to read, and enables properties and methods of the object to be accessed without issue.

Then it's a case of stepping down the hierarchy, checking at each step if one already exists with a matching id, and swapping the iterator to a reference to it if it does. This means that following code will add to the existing item.

In this particular case I've also added a FromThing function to allow easier combining of object properties.

Footton answered 15/4, 2020 at 17:36 Comment(0)
P
1

As we discussed in comments, this is an issue due to duplicate column names in two tables. This is where the similar issue and solution could be found. But, it does not include "mapping by code" as you said. So it is not exact duplicate.

I suggest you change the names of ID fields in your tables to avoid colliding them. Of-course, you should also change the name of your POCO properties and mappings accordingly.

If you cannot change the column names in table, change the POCO property name, and use the column alias in SQL query to match those new property names.

I hope this helps you.

Pelotas answered 7/1, 2019 at 14:40 Comment(0)
P
1

The problem was effectively the name of the properties.

I solved it using Custom Column Mapping to do it i got two possible solutions:

Without extensions

First, we define a Dictionary with the name of the column as key, and the name of the property as value

IDictionary<string, string> columnMaps = new Dictionary<string, string>()
            {
                { "Macro_tarea.ID", "ID" },
                { "Registros.ID", "ID" }
            };

Then, we define a delegate to obtain the PropertyInfo object of the property to which we intend to assign the alias of the previous dictionary

var mapper = new Func<Type, string, PropertyInfo>((type, columnName) =>
            {
                if (columnMaps.ContainsKey(columnName))
                    return type.GetProperty(columnMaps[columnName]);
                else
                    return type.GetProperty(columnName);
            });

Now, we define an object that implements the ITypeMap interface using CustomPropertyTypeMap implementation

ITypeMap MacroTareaMapper = new CustomPropertyTypeMap(typeof(Macro_tarea),
                (type, columnName) => mapper(type, columnName));

ITypeMap RegistrosMapper = new CustomPropertyTypeMap(typeof(Registros),
                (type, columnName) => mapper(type, columnName));

Then we register them

SqlMapper.SetTypeMap(typeof(Macro_tarea), MacroTareaMapper);
SqlMapper.SetTypeMap(typeof(Registros), RegistrosMapper);

Simpler solution with Dapper.FluentMap

It is implemented as follows:

We create a class that inherits from EntityMap<T> and using the Map method we define which column corresponds to each property. For example,

internal class Macro_tareaMap : EntityMap<Macro_tarea>
{
       internal Macro_tareaMap()
       {
            //Mi propiedad ID esta asociada a la columna Macro_tarea.ID
            Map(x => x.ID).ToColumn("Macro_tarea.ID");
       }
}

Then just register it

FluentMapper.Initialize((config) => 
{
    config.AddMap(new Macro_tareaMap());
});

Hope it helps another people!

Source: https://medium.com/dapper-net/custom-columns-mapping-1cd45dfd51d6

Puppis answered 7/1, 2019 at 16:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.