Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2: What should the API look like for horizontally mapped data via tuples? #745

Open
mgravell opened this issue Apr 21, 2017 · 7 comments
Open
Assignees
Labels
enhancement proposal v3.0 Changes awaiting the next breaking release
Milestone

Comments

@mgravell
Copy link
Member

mgravell commented Apr 21, 2017

Historically, we have the multi-generic Query<T1,T2, ... ,TResult> API for this which is ugly as sin and clunky to use.

We have an opportunity to improve things using tuples, with the nice advantage that since tuples are only one T (for any reasonable number of partitions), it won't cause overload explosion. Long term, this might even be seen as a replacement to the Query<T1,T2, ..., TResult> API (in a breaking major, etc).

I've thrown some early ideas into a branch; I want to invite feedback, or other original ideas - it is entirely possible that I'm missing an obvious + better way of doing this!

Again, the target scenario here is when you're selecting from multiple tables in one horizontal piece, and want to access the pieces into separate DTOs - in SQL terms:

select c.*, a.*
from Customer c
inner join CustomerAddresses ca on ca.CustomerId = c.Id
inner join Address a on a.Id = ca.AddressId

which is going to return a bunch of columns that we want to map (for each row) into a Customer object and an Address object.

Note: the discussion of what to do for duplicated Address records or Customer records (same Id, multiple instances vs single instance) is separate - please don't get distracted by that aspect (although that's also something we'd like to improve at a later data)

Here are a lot of examples in that test branch

@NickCraver
Copy link
Member

The very related API here (that I think sucks) is splitOn in these multi-map scenarios, e.g. splitOn: "Id, UserId" (defaults to splitOn: "Id"). It's string based and error-prone. Many people get it wrong once you leave the realm of Id being your PK column (or in the query). Some (IMO, bad) ideas:

  • We could have an array as the arg, but people are likely to allocate that on every call, likely resulting in: splitOn: new[] { nameof(Foo.Id), nameof(User.UserId) }
  • The Column/Table proposal in V2: [Column] and [Table] Attributes #722 could be used as the default. So instead of "Id" (still the default), it'd look for, effectively: splitOn: $"{Foo.PrimaryKeyField},{Bar.PrimaryKeyField}"
    • This would be determined by the [Column] attributes on those types (or the mapping function, if it handles PK recognition).
    • Con: obviously doesn't work well for multi-column PKs
    • Very "heavy" compared to a string

While these are more "safe" than a string, they're also by-simplest-usage more expensive. It'd be fantastic if we can pull a trick with tuples or something else that doesn't allocate so much and could get some of the safety features overall. Anyone have any fancy ideas for what they'd like that parameter to look like?

@NickCraver NickCraver changed the title What should the API look like for horizontally mapped data via tuples? V2: What should the API look like for horizontally mapped data via tuples? Apr 21, 2017
@NickCraver NickCraver added this to the v2.0 milestone Apr 21, 2017
@mgravell
Copy link
Member Author

mgravell commented Apr 21, 2017

Radical alternative - fluent API that separates the different aspects (command details, mapping details, etc)

var tuples = conn.Query(sql, args).Map<(tuple shape)>().ToList();
var orders = conn.Query(sql, arts).ToList<Order>();

Etc. Nothing would execute until the final ToList / AsEnumerable / Single etc - everything else is composition into structs. Note these are custom methods that just look like the familiar LINQ ones.

Discuss...

@rossipedia
Copy link

The fluent API looks really nice. What would the return type of Query(sql, args) be in that example?

@gbjbaanb
Copy link

Now C#7 has a syntactic sugary coating for tuples, it should look the same.

So a query would look like:

var resultrow = conn.Query<(string, int, int)>("select aString, anInt, anotherInt from aTable").ToList();

named items in the tuple would be specified in the tuple definition between the <>, or the default Item1, Item2 would be used otherwise.

I assume the compiler would do a lot of the work here if it knew where to see the tuple definition, but given functions that can now look like: (string first, string middle, string last) LookupName(long id) then the compiler happily handles this.

It might be possible to dynamically create the tuple from the query, but that won;t help the compiler, so I'm quite happy to strongly type it as I would if I was returning a POCO class, but in the case where you are returning a row of all columns (eg select *) then having each item in the tuple defined as a dynamically defined object sounds OK.

I don't like the idea of the fluent version - I want to call some SQL in 1 line of code and have some results back into a variable. Defining it in one place and then running the same SQL or defining the SQL in a string and passing it into multiple lines that all look the same doesn't feel right. I want the equivalent of "dapper, gimme data now" not setting up definitions, setting up control options, and then running the query.

Simple and consistent is good, so using the same mechanism C#7 now provides should be the default way, surely?!

@mgravell
Copy link
Member Author

mgravell commented Oct 23, 2017 via email

@TroySchmidt
Copy link

The improvement to "lose" splitOn can be dropped when using Dapper.Contrib in a future implementation. When adding the relationships and columns via LINQ or expression predicates the SQL generated will be required to keep the table alias prefix for all the columns. The internal mechanisms of adding those to the generated SQL can go so far as to keep them grouped together internally before using SqlBuilder. At that point the column to splitOn for all the generated queries using Dapper.Contrib in this manner are known.
This may not be the final implementation, but it does provide a nice layer of abstraction for those using Dapper.Contrib to not worry about defining splitOn.

@vbilopav
Copy link
Contributor

I've already implemented tuple mapping in my micro-orm project

I would be happy to help and contribute if there is a need.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement proposal v3.0 Changes awaiting the next breaking release
Projects
None yet
Development

No branches or pull requests

6 participants