Can't get multi-mapping to work in Dapper

marc_s picture marc_s · May 14, 2011 · Viewed 16.4k times · Source

Playing around with Dapper, I'm quite pleased with the results so far - intriguing!

But now, my next scenario would be to read data from two tables - a Student and an Address table.

Student table has a primary key of StudentID (INT IDENTITY), Address has an AddressID (INT IDENTITY). Student also has an FK called AddressID linking into the Address table.

My idea was to create two classes, one for each table, with the properties I'm interested in. Additionally, I put an PrimaryAddress property of type Address onto my Student class in C#.

I then tried to retrieve both student and address data in a single query - I mimick the sample that's given on the Github page:

var data = connection.Query<Post, User>(sql, (post, user) => { post.Owner = user; });
var post = data.First();

Here, a Post and a User are retrieved, and the owner of the post is set to the user - the type returned is a Post - correct?

So in my code, I define two parameters to the generic Query extension method - a Student as the first which should be returned, and an Address as the second, which will be stored onto the student instance:

var student = _conn.Query<Student, Address>
                  ("SELECT s.*, a.* FROM dbo.Student s 
                        INNER JOIN dbo.Address a ON s.AddressID = a.AddressID 
                        WHERE s.StudentenID = @Id", 
                    (stu, adr) => { stu.PrimaryAddress = adr; },  
                    new { Id = 4711 });

Trouble is - I get an error in Visual Studio:

Using the generic method 'Dapper.SqlMapper.Query(System.Data.IDbConnection, string, System.Func, dynamic, System.Data.IDbTransaction, bool, string, int?, System.Data.CommandType?)' requires 6 type arguments

I don't really understand why Dapper insists on using this overload with 6 type arguments...

Answer

Sam Saffron picture Sam Saffron · May 14, 2011

That would be cause I changed APIs and forgot to update the documentation, I corrected the error.

Be sure to have a look at Tests.cs for a full up-to-date spec.

In particular, the old API used to take in an Action<T,U> to perform the mapping, the trouble was that it felt both arbitrary and inflexible. You could not fully control the return type. The new APIs take in a Func<T,U,V>. So you can control the type you get back from the mapper and it does not need to be a mapped type.

I just tied up some additional flexibility around multi mapping, this test should make it clear:

class Person
{
    public int PersonId { get; set; }
    public string Name { get; set; }
}

class Address
{
    public int AddressId { get; set; }
    public string Name { get; set; }
    public int PersonId { get; set; }
}

class Extra
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public void TestFlexibleMultiMapping()
{
    var sql = 
@"select 
1 as PersonId, 'bob' as Name, 
2 as AddressId, 'abc street' as Name, 1 as PersonId,
3 as Id, 'fred' as Name
";
    var personWithAddress = connection.Query<Person, Address, Extra, Tuple<Person, Address,Extra>>
        (sql, (p,a,e) => Tuple.Create(p, a, e), splitOn: "AddressId,Id").First();

    personWithAddress.Item1.PersonId.IsEqualTo(1);
    personWithAddress.Item1.Name.IsEqualTo("bob");
    personWithAddress.Item2.AddressId.IsEqualTo(2);
    personWithAddress.Item2.Name.IsEqualTo("abc street");
    personWithAddress.Item2.PersonId.IsEqualTo(1);
    personWithAddress.Item3.Id.IsEqualTo(3);
    personWithAddress.Item3.Name.IsEqualTo("fred");

}

Dapper pipes all the multi mapping APIs through a single method, so if something fails it will end up in the 6 param one. The other piece of the puzzle was that I did not allow for some super flexible splits, which I just added.

Note, the splitOn param will default to Id, meaning it will take a column called id or Id as the first object boundary. However if you need boundaries on multiple primary keys that have different names for say a "3 way" multi mapping, you can now pass in a comma separated list.

So if we were to fix the above, probably the following would work:

 var student = _conn.Query<Student,Address,Student>
              ("SELECT s.*, a.* FROM dbo.Student s 
                    INNER JOIN dbo.Address a ON s.AddressID = a.AddressID 
                    WHERE s.StudentenID = @Id", 
                (stu, adr) => { stu.PrimaryAddress = adr; return stu;},  
                new { Id = 4711 }, splitOn: "AddressID").FirstOrDefault();