-
Notifications
You must be signed in to change notification settings - Fork 5
Transforming query results
The query result is always tabular. Even though ADO.NET allows to execute many SQL commands at once, their in-memory representation form much more complex, hierarchical structures.
Fortunately, queries are functions and as such, they can be composed with other functions using >>
and |>
operators.
Thus, result transformations are functions too:
- one group is explicit but rather verbose
- another group, based on conventions, uses reflection and code generation to access key fields, etc...
Most transformations fall into four categories.
Function, returning two results, must return tuple:
let getPostsWithTags: int -> DataContext -> Post * Tag list =
sql "select id, blogId, name, title, content, author,
createdAt, modifiedAt, modifiedBy, status
from post
where blogId = @id;
select t.postId, t.name
from tag t join post p on t.postId = p.id
where p.blogId = @id"
The result can be transformed with the join
function:
let getPostsWithTags: int -> DataContext -> Post list Async =
sql "select id, blogId, name, title, content, author,
createdAt, modifiedAt, modifiedBy, status
from post
where blogId = @id;
select t.postId, t.name
from tag t join post p on t.postId = p.id
where p.blogId = @id"
>> AsyncDb.map (join (fun p -> p.id)
(fun t -> t.postId)
(fun p t -> { p with tags = t }))
When following some simple naming conventions, i.e.
- the join key has
Id
,<Parent>Id
or '_id` name in parent record (case insensitive), - the join key has
<Parent>Id
or<Parent>_id
in child record (case insensitive), - exactly one proprty of
<Child>
list type exists in parent record,
the more concise join version can be used:
let getPostsWithTags: int -> DataContext -> Post list Async =
sql "select id, blogId, name, title, content, author,
createdAt, modifiedAt, modifiedBy, status
from post
where blogId = @id;
select t.postId, t.name
from tag t join post p on t.postId = p.id
where p.blogId = @id"
>> AsyncDb.map join<_, Tag>
The result, that is a product, can be represented as a list of tuples:
let getPostsWithTags: int -> DataContext -> (Post * Tag) list Async =
sql "select p.id, p.blogId, p.name, p.title, p.content, p.author,
p.createdAt, p.modifiedAt, p.modifiedBy, p.status,
t.postId as item_postId, t.name as item_name
from post p left join tag t on t.postId = p.id
where p.id = @id"
It can be transformed using grouping by first item of a tuple:
let getPostsWithTags: int -> DataContext -> Post list Async =
sql "select p.id, p.blogId, p.name, p.title, p.content, p.author,
p.createdAt, p.modifiedAt, p.modifiedBy, p.status,
t.postId as item_postId, t.name as item_name
from post p left join tag t on t.postId = p.id
where p.id = @id"
>> AsyncDb.map (group (fun p t -> { p with tags = aliasedAsItem t }))
In the example above, there are two 'name' columns. To make it possible to use aliasing, we use aliasedAsItem
function, that allows to use item_{column name} aliases for detail data. There are also group3
and group4
functions.
There is, of course, convention-based approach, that works when no tranformation of child list is needed and only one child list collection appears in a parent record:
let getPostsWithTags: int -> DataContext -> Post list Async =
sql "select p.id, p.blogId, p.name, p.title, p.content, p.author,
p.createdAt, p.modifiedAt, p.modifiedBy, p.status,
t.postId, t.name as tagName
from post p left join tag t on t.postId = p.id
where p.id = @id"
>> AsyncDb.map group<_, Tag>
When reading one object and some detail data, no joining or grouping is needed. In this case, we can use ordinary function:
let getBlogWithPosts: int -> DataContext -> Blog Async =
sql "select id, name, title, description, owner,
createdAt, modifiedAt, modifiedBy
from Blog
where id = @id;
select id, blogId, name, title, content, author,
createdAt, modifiedAt, modifiedBy, status
from post
where blogId = @id"
>> AsyncDb.map (fun b p -> { b with posts = p })
As with join, when exactly one property of <Child> list
exists, more concise way can be used:
let getBlogWithPosts: int -> DataContext -> Blog Async =
sql "select id, name, title, description, owner,
createdAt, modifiedAt, modifiedBy
from Blog
where id = @id;
select id, blogId, name, title, content, author,
createdAt, modifiedAt, modifiedBy, status
from post
where blogId = @id"
>> AsyncDb.map combine<_, Post>
Sometimes more, than two results should be joined. In this case we can use a mechanism, that combine joins:
let getPostsWithTagsAndComments: int -> DataContext -> Post list Async =
sql "select id, blogId, name, title, content, author,
createdAt, modifiedAt, modifiedBy, status
from post
where blogId = @id;
select t.postId, t.name
from tag t join post p on t.postId = p.id
where p.blogId = @id;
select c.id, c.postId, c.parentId, c.content, c.author, c.createdAt
from comment c join post p on c.postId = p.id
where p.blogId = @id"
>> AsyncDb.map (join<_, Tag> >-> join<_, Comment>)
The >->
operator combines two functions by passing result of the first one as a first argument of the second one:
let (>->) f g = fun (t1, t2) -> g(f t1, t2)
It allows to build a chain of functions, that subsequently transform result of the same type.
There is also another operator: >>-
, that passes result of the second function as a second argument of the first function:
let (>>-) f g = fun (t1, t2) -> f(t1, g(t2))
It allows to define deep hierarchies.
In the example below the >>-
operator is used to build nested hierarchy of posts with tags and comments, then add them to a blog:
let getBlogWithPostsWithTagsAndComments: int -> DataContext -> Blog Async =
sql "select id, name, title, description, owner,
createdAt, modifiedAt, modifiedBy
from Blog
where id = @id
select id, blogId, name, title, content, author,
createdAt, modifiedAt, modifiedBy, status
from post
where blogId = @id;
select t.postId, t.name
from tag t join post p on t.postId = p.id
where p.blogId = @id;
select c.id, c.postId, c.parentId, c.content, c.author, c.createdAt
from comment c join post p on c.postId = p.id
where p.blogId = @id"
>> AsyncDb.map (update<_, Post> >>- (join<_, Tag> >-> join<_, Comment>))
It's also possible to mix joins, groups and updates in one result transformation.
There are two functions transforming tuples, returned from stored procedures:
resultOnly (map: 't -> 'u) (_: int, (), result: 't)
that ignores return code and output parameters, and returns query results with possible mapping function, and:
outParamsOnly (_: int, outParams, ())
that ignores return code and results, and returns output parameters. They can be called the same way, as other transformations:
let findPosts: (PostCriteria * SignatureCriteria) -> DataContext -> Post list Async =
storedproc "FindPosts"
>> AsyncDb.map (resultOnly id)