Skip to content

Commit 28ae4ff

Browse files
committed
Added an Aggregation section to the tips documentation page.
1 parent 33d5b74 commit 28ae4ff

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed

Diff for: docs/tips.rst

+183
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,186 @@ some of the allowed queries are
8585
}
8686
}
8787
88+
89+
Aggregating
90+
-----------
91+
92+
By default the `sqlalchemy.orm.Query` object that is retrieved through the `SQLAlchemyObjectType.get_query` method auto-selects the underlying SQLAlchemy ORM class. In order to change fields under the `SELECT` statement, e.g., when performing an aggregation, one can retrieve the `sqlalchemy.orm.Session` object from the provided `info` argument and create a new query as such:
93+
94+
.. code::
95+
96+
session = info.context.get("session") # type: sqlalchemy.orm.Session
97+
query = session.query(SomeOtherModel, some_aggregation_function)
98+
99+
Consider the following SQLAlchemy ORM models:
100+
101+
.. code::
102+
103+
class Author(Base):
104+
__tablename__ = "authors"
105+
106+
author_id = sqlalchemy.Column(
107+
sqlalchemy.types.Integer(),
108+
primary_key=True,
109+
)
110+
111+
name_first = sqlalchemy.Column(
112+
sqlalchemy.types.Unicode(length=80),
113+
nullable=False,
114+
)
115+
116+
name_last = sqlalchemy.Column(
117+
sqlalchemy.types.Unicode(length=80),
118+
nullable=False,
119+
)
120+
121+
books = sqlalchemy.orm.relationship(
122+
argument="Book",
123+
secondary="author_books",
124+
back_populates="authors",
125+
)
126+
127+
128+
class Book(Base):
129+
__tablename__ = "books"
130+
131+
book_id = sqlalchemy.Column(
132+
sqlalchemy.types.Integer(),
133+
primary_key=True,
134+
)
135+
136+
title = sqlalchemy.Column(
137+
sqlalchemy.types.Unicode(length=80),
138+
nullable=False,
139+
)
140+
141+
year = sqlalchemy.Column(
142+
sqlalchemy.types.Integer(),
143+
nullable=False,
144+
)
145+
146+
cover_artist = sqlalchemy.Column(
147+
sqlalchemy.types.Unicode(length=80),
148+
nullable=True,
149+
)
150+
151+
authors = sqlalchemy.orm.relationship(
152+
argument="Author",
153+
secondary="author_books",
154+
back_populates="books",
155+
)
156+
157+
158+
class AuthorBook(Base):
159+
__tablename__ = "author_books"
160+
161+
author_book_id = sqlalchemy.Column(
162+
sqlalchemy.types.Integer(),
163+
primary_key=True,
164+
)
165+
166+
author_id = sqlalchemy.Column(
167+
sqlalchemy.types.Integer(),
168+
sqlalchemy.ForeignKey("authors.author_id"),
169+
index=True,
170+
)
171+
172+
book_id = sqlalchemy.Column(
173+
sqlalchemy.types.Integer(),
174+
sqlalchemy.ForeignKey("books.book_id"),
175+
index=True,
176+
)
177+
178+
exposed to the GraphQL schema through the following types:
179+
180+
.. code::
181+
182+
class TypeAuthor(SQLAlchemyObjectType):
183+
class Meta:
184+
model = Author
185+
186+
187+
class TypeBook(SQLAlchemyObjectType):
188+
class Meta:
189+
model = Book
190+
191+
192+
class TypeAuthorBook(SQLAlchemyObjectType):
193+
class Meta:
194+
model = AuthorBook
195+
196+
If we wanted to perform an aggregation, e.g., count the number of books by cover-artist, we'd first define such a custom type:
197+
198+
.. code::
199+
200+
class TypeCountBooksCoverArtist(graphene.ObjectType):
201+
cover_artist = graphene.String()
202+
count_books = graphene.Int()
203+
204+
which we can then expose through a class deriving `graphene.ObjectType` as follows:
205+
206+
.. code::
207+
208+
class TypeStats(graphene.ObjectType):
209+
210+
count_books_by_cover_artist = graphene.List(
211+
of_type=TypeCountBooksCoverArtist
212+
)
213+
214+
@staticmethod
215+
def resolve_count_books_by_cover_artist(
216+
args: Dict,
217+
info: graphql.execution.base.ResolveInfo,
218+
) -> List[TypeCountBooksCoverArtist]:
219+
# Retrieve the session out of the context as the `get_query` method
220+
# automatically selects the model.
221+
session = info.context.get("session") # type: sqlalchemy.orm.Session
222+
223+
# Define the `COUNT(books.book_id)` function.
224+
func_count_books = sqlalchemy_func.count(Book.book_id)
225+
226+
# Query out the count of books by cover-artist
227+
query = session.query(Book.cover_artist, func_count_books)
228+
query = query.group_by(Book.cover_artist)
229+
results = query.all()
230+
231+
# Wrap the results of the aggregation in `TypeCountBooksCoverArtist`
232+
# objects.
233+
objs = [
234+
TypeCountBooksCoverArtist(
235+
cover_artist=result[0],
236+
count_books=result[1]
237+
) for result in results
238+
]
239+
240+
return objs
241+
242+
As can be seen, the `sqlalchemy.orm.Session` object is retrieved from the `info.context` and a new query specifying the desired field and aggregation function is defined. The results of the aggregation do not directly correspond to an ORM class so they're wrapped in the `TypeCountBooksCoverArtist` class and returned.
243+
244+
The `TypeStats` class can then be exposed under the `Query` class as such:
245+
246+
.. code::
247+
248+
class Query(graphene.ObjectType):
249+
250+
stats = graphene.Field(type=TypeStats)
251+
252+
@staticmethod
253+
def resolve_stats(
254+
args: Dict,
255+
info: graphql.execution.base.ResolveInfo,
256+
):
257+
return TypeStats
258+
259+
thus allowing for the following query:
260+
261+
.. code::
262+
263+
query getCountBooksByCoverArtist{
264+
stats {
265+
countBooksByCoverArtist {
266+
coverArtist,
267+
countBooks
268+
}
269+
}
270+
}

0 commit comments

Comments
 (0)