Just trying out F# 3.0 and hit a bit of a wall when it comes to grouping by multiple columns. The obvious thing to try was
query {
for d in context.table do
groupBy (d.col1,d.col2) into g
select (g.Key)
}
But I get a "Only parameterless constructors and initializers are supported in LINQ to Entities." exception.
I can't seem to find an example on msdn
http://msdn.microsoft.com/en-us/library/hh225374(v=vs.110).aspx
http://msdn.microsoft.com/en-us/library/hh361035(v=vs.110).aspx
And I realize my question is similar to " Entity Framework and Anonymous Types in F#" but it seems to be powerpack/F#2.x focused and I'm hoping F# 3.0 has an elegant answer... Any ideas?
UPDATE:
I came across the CLIMutable attribute from reading Brian's post at:
http://blogs.msdn.com/b/fsharpteam/archive/2012/07/19/more-about-fsharp-3.0-language-features.aspx
I was pretty optimistic so I tried
[<CLIMutable>]
type MyRecord = { Column1 : int; Column2 : int }
query {
for d in context.table do
groupBy {Column1 = col1; Column2 = col2} into g
select (g.Key)
}
Unfortunately I get the exact same exception.
The following is an example of multiple columns being used for grouping in c# and converted to f# (overly paranoid management has made me rename everything, but I believe I have been consistent):
(TheDatabase was generated by SqlMetal, GetSummedValuesResult is a F# record type)
c#
public static class Reports
{
public static IReadOnlyList<GetSummedValuesResult> GetSummedValues(TheDatabase db, DateTime startDate, DateTime? endDate)
{
var query =
from sv in db.SomeValues
where (sv.ADate >= startDate && sv.ADate <= (endDate ?? startDate))
group sv by new { sv.ADate, sv.Owner.Name } into grouping
select new GetSummedValuesResult(
grouping.Key.ADate,
grouping.Key.Name,
grouping.Sum(g => g.Value)
);
return query.ToList();
}
}
f#
type Reports() =
static member GetSummedValues (db:TheDatabase) startDate (endDate:Nullable<DateTime>) =
let endDate = if endDate.HasValue then endDate.Value else startDate
let q = query {
for sv in db.SomeValues do
where (sv.ADate >= startDate && sv.ADate <= endDate)
let key = AnonymousObject<_,_>(sv.ADate, sv.Owner.Name)
groupValBy sv key into grouping
select {
ADate = grouping.Key.Item1;
AName = grouping.Key.Item2;
SummedValues = grouping.Sum (fun (g:TheDatabaseSchema.SomeValues) -> g.Value)
}
}
List(q) :> IReadOnlyList<GetSummedValuesResult>
So the thing to use is Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject
Note that you should not use the Seq module for aggregation functions!!
SummedValues = grouping |> Seq.sumBy (fun g -> g.SomeValues)
Although this WILL WORK, it does the aggregation on the client side, rather than formulating appropriate SQL.
I see this in first of your links, I think it is what you want:
query {
for student in db.Student do
groupValBy student.Name student.Age into g
select (g, g.Key, g.Count())
}
student.Age
. The first arg groupValBy
is the value which will be stored, the second is the key to group by. Imagine the word by
between the first and second args. - latkin
When using groupBy you need to select an aggregating function (e.g. count, sum, avg...).
First you have to remember that a query is translated into actual SQL
at some point. It appears that linq does not support the use of multiple group keys as a Tuple<>
. Therefore any transformation into Tuple<>
has to be done after the database call has completed.
Second, you should be able to achieve multiple key grouping by performing multiple groupings behind each other on the respective keys:
query {
for d1 in context.table do
groupBy d1.col1 into g1
for d2 in g1 do
groupBy d2.col2 into g2
select g2
}
Please have mercy with me if the syntax is not 100% since F# is not my native tongue :) The concept however should work just fine.
open Microsoft.FSharp.Linq.RuntimeHelpers
open System.Linq
query {
for d in context.table do
let t = MutableTuple<_,_>(Item1=d.col1,Item2=d.col2)
groupValBy d t into g
select (g.Key,g.Count())
}
//in F# 3.0
open Microsoft.FSharp.Linq.RuntimeHelpers
open Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter
open System.Linq
//[<CLIMutable>]
//type MyRecord = { Column1 : int; Column2 : int }
// require constructor in F#
// groupBy is not valid
type T(column1 : int, column2 : int)
member val Colum1=colum1 with get,set
membre val Colum2=colum2 with get,set
query {
for d in context.table do
groupValBy d (NewAnonymousObjectHelper(T(d.Colum1,d.Colume2))) into g
select (g.Key)
}
query {
for d in context.table do
groupBy (new {d.col1, d.col2}) into g
select (g.Key)
}
new {d.col1, d.col2}
constructs a C# anonymous type; this is not valid F#. - Jay
g.Count()
instead and see what happens. I haven't tried 3.0...just guessing. - Danielg.Key
is a tuple, which doesn't have a parameterless constructor.select 1
should work though, if my guess is correct. - Danielselect g.Key
does not construct or initialize a new tuple, however. AFAICT,select (g.Key)
would be aTuple<Tuple<col1_t, col2_t>>
, which is invoking the constructor ofTuple<'T1>
;select g.Key
would be aTuple<col1_t, col2_t>
, reusing an existing value instead of initializing a new one. Of course, I'm running with the assumption that the exception is being caused by theselect
rather than thegroupBy
, which may be totally incorrect. :-P - ildjarn