Filters: Aggregations #4843
Replies: 36 comments 2 replies
-
Aggregations
IntroductionAggregations are very useful for querying big data sources. To count items or sum a value, it's not necessary to load all the data. Querying already aggregated data in the backend has less overhead. ProposalA new API Endpoint is generated for a collection. The type of this endpoint has different aggregation fields on them. It will not be possible to cover all aggregation cases. We simply cannot create types for all possible scenarios. Proposed are the following: Example ObjectsAll examples will take place in the following domain: public class Foo {
public string role {get;set;}
public int num {get;set;}
public Bar bars {get;set;}
}
public class Bar {
public string baz {get;set;}
public int blub {get;set;}
} count{
foosMeta {
count
} returns the amount of foo's in foos. sum{
foosMeta {
sum_num
} returns the sum of all nums. average{
foosMeta {
sum_average
} returns the average of all nums. group_byExample{
foosMeta {
group_by {
role {
key
value {
bar {
baz
}
}
}
}
}
} How others did itPrisma (Issue prisma/prisma#1312)# Count all posts with a title containing 'GraphQL'
query {
postsConnection(where: {
title_contains: "GraphQL"
}) {
aggregate {
count
}
}
}
{
allUsersConnection(where: {city: "Aarhus"}) {
aggregate {
avg {
age
}
max {
age
}
}
edges {
node { name, age }
}
}
}
{
allUsersConnection(where: {city: "Aarhus"}) {
aggregate {
avg {
age
}
max {
age
}
}
edges {
node { name, age }
}
}
} Horsuraquery {
article_aggregate {
aggregate {
count
sum {
rating
}
avg {
rating
}
max {
rating
}
}
nodes {
id
title
rating
}
}
}
query {
author (where: {id: {_eq: 1}}) {
id
name
articles_aggregate {
aggregate {
count
avg {
rating
}
max {
rating
}
}
nodes {
id
title
rating
}
}
}
} Gatsby### no example found GraphCMS### no example found |
Beta Was this translation helpful? Give feedback.
-
In your average example, I guess you mean |
Beta Was this translation helpful? Give feedback.
-
@angrymrt You are right it would be We usually first work on the graphql API specification before we do the code API. {
allUsersConnection(where: {city: "Aarhus"}) {
aggregate {
avg {
age
}
max {
age
}
}
edges {
node { name, age }
}
}
} |
Beta Was this translation helpful? Give feedback.
-
I currently think also that we could go down the rout of lodash for aggregations. We definitely will add support in the client for that. |
Beta Was this translation helpful? Give feedback.
-
Hi all, just wondering what the status of this issue is, and if there hasn't been any progress I would love to offer some assistance. I just need to be pointed in the right direction 😄 |
Beta Was this translation helpful? Give feedback.
-
Hi @kkliebersbach |
Beta Was this translation helpful? Give feedback.
-
I'm really waiting for this feature. |
Beta Was this translation helpful? Give feedback.
-
I have to implement some kind of aggregation in the next 2 week and was going to try and do the Hasura syntax. @PascalSenn do you have any guidance on how to even define the ObjectTypes for that from a HotChocolate perspective? All my other current code is a code-first implementation so I'd like to stick with that if possible but its not clear to me how to define it. Did you maybe start a prototype that is available to look at? |
Beta Was this translation helpful? Give feedback.
-
@shawndube You can have a look at the Data project. Filtering is a good example. |
Beta Was this translation helpful? Give feedback.
-
@PascalSenn we are seriously waiting for that feature |
Beta Was this translation helpful? Give feedback.
-
Cross-posting my comment in #1260 (comment) which is also for aggregations but for sorting
|
Beta Was this translation helpful? Give feedback.
-
@PascalSenn do you know approximately when we can expect this feature to be implemented? E.g: is it closer to 1 month or 1 year? |
Beta Was this translation helpful? Give feedback.
-
@tjolr this feature can be easily done on your own with type extensions or type interceptors. We have put aggregates towards the end of this year since there is really not a so big need to have this in the sense that it is easy to build this on your own. However, we will do it and have scheduled it for the November release. No promises, it might be moved to an earlier or later release. We in general look at feedback of the community to determine the need of a feature. What we could however do is write up a blog post outlining how one could implement this. @PascalSenn what do you think? |
Beta Was this translation helpful? Give feedback.
-
@michaelstaib a blog post about how this can be done would be good! 👍 But we can't wait for the official implementation of the data aggregation as well!! :-) 💯 |
Beta Was this translation helpful? Give feedback.
-
Hi @ademchenko and @symeus-sgs and @LordLyng. I also came across the issue on aggregation fields, where I would like to count the total entries of a field (not just the numbers of entries returning in pagination). I am not sure whether my scenario is the same as your scenario, but at least I managed to get the aggregated field response that fit into my use case. I will use the example at graphql-workshop fluent type configurations for explanation. in descriptor
.Field("sessionsCount")
.ResolveWith<SpeakerResolvers>(t => t.GetSessionsCountAsync(default!, default!, default!, default))
.UseDbContext<ApplicationDbContext>(); and in the public async Task<IEnumerable<int>> GetSessionsCountAsync(
Speaker speaker,
[ScopedService] ApplicationDbContext dbContext,
SessionByIdDataLoader sessionById,
CancellationToken cancellationToken)
{
int[] sessionIds = await dbContext.Speakers
.Where(s => s.Id == speaker.Id)
.Include(s => s.SessionSpeakers)
.SelectMany(s => s.SessionSpeakers.Select(t => t.SessionId))
.ToArrayAsync();
return sessionsIds.Length; //Side note here, you can mess up with anykinds of aggreation you want
} viola! the aggregated value will now be returned in the graphQL response. |
Beta Was this translation helpful? Give feedback.
-
We are introducing a new projection engine with V13 that will open up the engine to do proper aggregations. We are still experimenting but it looks promising. |
Beta Was this translation helpful? Give feedback.
-
@chungonion , the issue is not about including into response some custom field which could be an aggregation field or any other one. The issue is to build the aggregation over the filtered ([UseFiltering]) collection- so, to reuse the actual filter to build the aggregated values. @michaelstaib could you or your teammates, please, explain how to do that in version 12, because version 13 is too far for us. |
Beta Was this translation helpful? Give feedback.
-
using System;
using System.Collections.Generic;
using System.Linq;
using HotChocolate;
using HotChocolate.Data;
using HotChocolate.Data.Filters.Expressions;
using HotChocolate.Data.Sorting.Expressions;
using HotChocolate.Resolvers;
using HotChocolate.Types;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Test
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services
.AddRouting()
.AddGraphQLServer()
.AddQueryType<Query>()
.AddTypeExtension<FooConntectionExtensions>()
.AddFiltering()
.AddSorting()
.AddProjections();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseWebSockets();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
// By default the GraphQL server is mapped to /graphql
// This route also provides you with our GraphQL IDE. In order to configure the
// the GraphQL IDE use endpoints.MapGraphQL().WithToolOptions(...).
endpoints.MapGraphQL();
});
}
}
[ExtendObjectType(WellKnownTypeNames.FooConnection)]
public class FooConntectionExtensions
{
public int TotalOfNumbers([ScopedState(WellKnownContextData.Data)] IEnumerable<Foo> foo) =>
foo.Sum(x => x.Number);
public int MaxOfNumbers([ScopedState(WellKnownContextData.Data)] IEnumerable<Foo> foo) =>
foo.Max(x => x.Number);
public int MinOfNumbers([ScopedState(WellKnownContextData.Data)] IEnumerable<Foo> foo) =>
foo.Min(x => x.Number);
}
public static class WellKnownContextData
{
public const string Data = nameof(Data);
}
public static class WellKnownTypeNames
{
public const string FooConnection = nameof(FooConnection);
}
public class Query
{
[UsePaging(ConnectionName = "Foo")]
[UseProjection]
[UseFiltering]
[UseSorting]
public IEnumerable<Foo> GetFoos(IResolverContext context)
{
IEnumerable<Foo> result = _data.Filter(context).Sort(context);
context.ScopedContextData =
context.ScopedContextData.SetItem(WellKnownContextData.Data, result);
return result;
}
private static IEnumerable<Foo> _data = new Foo[]
{
new(Guid.NewGuid(), 1, "A"), new(Guid.NewGuid(), 2, "A"),
new(Guid.NewGuid(), 4, "A"), new(Guid.NewGuid(), 3, "A")
};
}
public record Foo
{
public Foo()
{
}
public Foo(Guid id, int number, string name)
{
Id = id;
Name = name;
Number = number;
}
public Guid Id { get; init; }
public int Number { get; init; }
public string Name { get; init; }
}
} |
Beta Was this translation helpful? Give feedback.
-
@PascalSenn does the same approach work for OffsetPaging? |
Beta Was this translation helpful? Give feedback.
-
Waiting for this feature, or the blog post explaining how to do this on our end. |
Beta Was this translation helpful? Give feedback.
-
@MaheshB0ngani look at the code examples pascal posted. |
Beta Was this translation helpful? Give feedback.
-
How you can add aggregation fields to the Connection type is also documented here. |
Beta Was this translation helpful? Give feedback.
-
@MaheshB0ngani which API for filtering do you use? |
Beta Was this translation helpful? Give feedback.
-
Hi @michaelstaib Thank you for your quick response. I was expecting a feature like group by on the fields the type and which has to be executed on the Database using Entity Framework and IQueryable.
User model may look like
I know the example I used might be silly. However, my original requirement is exactly similar.
and lot more, fully customizable from our UI. We need to be as generic as possible. I love the way HotChocolate will do filtering, Sorting and Pagination on top of EF & IQueryable. And also expected HotChocolate would have something out of the box for us to do Aggregations (Dynamic Group by) in very simple manner. Note: I'm using Entity Framework Core to query the database and expecting the data to be queries on the Database and not in-memory after fetching into .Net application. Please guide me with an example of how to implement these kind of dynamic grouping things. |
Beta Was this translation helpful? Give feedback.
-
Any update on this? When we can expect this? |
Beta Was this translation helpful? Give feedback.
-
We have a similar use case to @ademchenko and others in this thread, in that we don't want aggregates on the paginated result set, we want aggregates on the entire table, with filters applied. Use case: I want to render paginated JobParts table, where JobParts are assigned to a particular job. I also want to show the total cost for all JobParts assigned to a particular job. For this use case we need aggregates applied using the filtered (non-paginated) results. This is the approach we took (which is actually very similar to @PascalSenn's approach): query {
jobParts(where: { jobId: { eq: 1 }}) {
items {
id
cost
}
aggregates {
cost {
sum
}
}
}
}
Here's our parent JobParts resolver: [ExtendObjectType(OperationTypeNames.Query)]
public class JobPartQuery
{
[UseDbContext(typeof(TenantDbContext))]
[Authorize(Policy = Auth.Authorization.Permissions.RepairsRead)]
[UseOffsetPaging]
[UseProjection]
[UseFiltering]
[UseSorting]
[GraphQLDescription("Provides a list of Job Parts")]
public IQueryable<JobPart> JobParts(
[ScopedService] TenantDbContext dbContext,
IResolverContext context
)
{
var jobParts = dbContext.JobParts.AsNoTracking().Filter(context);
// Required for aggregates
context.SetScopedValue(JobPartContextData.JobPartsQueryExpression, jobParts.Expression);
return jobParts;
}
}
[ExtendObjectType("JobPartCollectionSegment")]
public class JobPartAggregatesCollectionSegment
{
public JobPartAggregates Aggregates()
{
return new JobPartAggregates();
}
} We then bind aggregate fields to resolvers in our descriptor.Field(input => input.Cost)
.Type<AggregateType<decimal, FloatType>>()
.ResolveWith<JobPartAggregatesQuery>(r => r.Cost(default, default))
.Description("JobPart Cost aggregates."); In our aggregate resolver we create a new query using the Filtered public class JobPartAggregatesQuery
{
[UseDbContext(typeof(TenantDbContext))]
public async Task<Aggregate<decimal>> Cost(
[ScopedService] TenantDbContext dbContext,
[ScopedState(JobPartContextData.JobPartsQueryExpression)] Expression queryExpression
)
{
var jobParts = dbContext
.GetService<IAsyncQueryProvider>()
.CreateQuery<JobPart>(queryExpression)
.AsNoTracking();
return await jobParts
.GroupBy(x => 1)
.Select(grp => new Aggregate<decimal>
{
Min = grp.Min(x => x.Cost),
Max = grp.Max(x => x.Cost),
Sum = grp.Sum(x => x.Cost)
})
.SingleAsync();
}
} It's a bit hacky but it works. Let me know if you have any suggestions to improve this. |
Beta Was this translation helpful? Give feedback.
-
Any updates? I'm looking forward for this feature |
Beta Was this translation helpful? Give feedback.
-
Any updates? |
Beta Was this translation helpful? Give feedback.
-
Any updates on this? How can I apply the aggregations after the projection? I haven't found any examples yet. |
Beta Was this translation helpful? Give feedback.
-
Scroll down for the description :)
Beta Was this translation helpful? Give feedback.
All reactions