From 28ae4ff0bbb901b1624ed45a5195762e131d16f1 Mon Sep 17 00:00:00 2001 From: Adamos Kyriakou Date: Wed, 3 Oct 2018 13:43:42 +1000 Subject: [PATCH] Added an `Aggregation` section to the `tips` documentation page. --- docs/tips.rst | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/docs/tips.rst b/docs/tips.rst index 1e8d0a6c..d110b48b 100644 --- a/docs/tips.rst +++ b/docs/tips.rst @@ -85,3 +85,186 @@ some of the allowed queries are } } + +Aggregating +----------- + +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: + +.. code:: + + session = info.context.get("session") # type: sqlalchemy.orm.Session + query = session.query(SomeOtherModel, some_aggregation_function) + +Consider the following SQLAlchemy ORM models: + +.. code:: + + class Author(Base): + __tablename__ = "authors" + + author_id = sqlalchemy.Column( + sqlalchemy.types.Integer(), + primary_key=True, + ) + + name_first = sqlalchemy.Column( + sqlalchemy.types.Unicode(length=80), + nullable=False, + ) + + name_last = sqlalchemy.Column( + sqlalchemy.types.Unicode(length=80), + nullable=False, + ) + + books = sqlalchemy.orm.relationship( + argument="Book", + secondary="author_books", + back_populates="authors", + ) + + + class Book(Base): + __tablename__ = "books" + + book_id = sqlalchemy.Column( + sqlalchemy.types.Integer(), + primary_key=True, + ) + + title = sqlalchemy.Column( + sqlalchemy.types.Unicode(length=80), + nullable=False, + ) + + year = sqlalchemy.Column( + sqlalchemy.types.Integer(), + nullable=False, + ) + + cover_artist = sqlalchemy.Column( + sqlalchemy.types.Unicode(length=80), + nullable=True, + ) + + authors = sqlalchemy.orm.relationship( + argument="Author", + secondary="author_books", + back_populates="books", + ) + + + class AuthorBook(Base): + __tablename__ = "author_books" + + author_book_id = sqlalchemy.Column( + sqlalchemy.types.Integer(), + primary_key=True, + ) + + author_id = sqlalchemy.Column( + sqlalchemy.types.Integer(), + sqlalchemy.ForeignKey("authors.author_id"), + index=True, + ) + + book_id = sqlalchemy.Column( + sqlalchemy.types.Integer(), + sqlalchemy.ForeignKey("books.book_id"), + index=True, + ) + +exposed to the GraphQL schema through the following types: + +.. code:: + + class TypeAuthor(SQLAlchemyObjectType): + class Meta: + model = Author + + + class TypeBook(SQLAlchemyObjectType): + class Meta: + model = Book + + + class TypeAuthorBook(SQLAlchemyObjectType): + class Meta: + model = AuthorBook + +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: + +.. code:: + + class TypeCountBooksCoverArtist(graphene.ObjectType): + cover_artist = graphene.String() + count_books = graphene.Int() + +which we can then expose through a class deriving `graphene.ObjectType` as follows: + +.. code:: + + class TypeStats(graphene.ObjectType): + + count_books_by_cover_artist = graphene.List( + of_type=TypeCountBooksCoverArtist + ) + + @staticmethod + def resolve_count_books_by_cover_artist( + args: Dict, + info: graphql.execution.base.ResolveInfo, + ) -> List[TypeCountBooksCoverArtist]: + # Retrieve the session out of the context as the `get_query` method + # automatically selects the model. + session = info.context.get("session") # type: sqlalchemy.orm.Session + + # Define the `COUNT(books.book_id)` function. + func_count_books = sqlalchemy_func.count(Book.book_id) + + # Query out the count of books by cover-artist + query = session.query(Book.cover_artist, func_count_books) + query = query.group_by(Book.cover_artist) + results = query.all() + + # Wrap the results of the aggregation in `TypeCountBooksCoverArtist` + # objects. + objs = [ + TypeCountBooksCoverArtist( + cover_artist=result[0], + count_books=result[1] + ) for result in results + ] + + return objs + +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. + +The `TypeStats` class can then be exposed under the `Query` class as such: + +.. code:: + + class Query(graphene.ObjectType): + + stats = graphene.Field(type=TypeStats) + + @staticmethod + def resolve_stats( + args: Dict, + info: graphql.execution.base.ResolveInfo, + ): + return TypeStats + +thus allowing for the following query: + +.. code:: + + query getCountBooksByCoverArtist{ + stats { + countBooksByCoverArtist { + coverArtist, + countBooks + } + } + }