Package: graphjoiner
In the reference GraphQL implementation, resolve functions describe how to
fulfil some part of the requested data for each instance of an object.
If implemented naively with a SQL backend, this results in the N+1 problem.
For instance, given the query:{
    books(genre: "comedy") {
        title
        author {
            name
        }
    }
}A naive GraphQL implementation would issue one SQL query to get the list of all
books in the comedy genre, and then N queries to get the author of each book
(where N is the number of books returned by the first query).There are various solutions proposed to this problem: GraphJoiner suggests that
using joins is a natural fit for many use cases. For this specific case, we only
need to run two queries: one to find the list of all books in the comedy genre,
and one to get the authors of books in the comedy genre.ExampleLet’s say we have some models defined by SQLAlchemy. A book has an ID, a title,
a genre and an author ID. An author has an ID and a name.fromsqlalchemyimportColumn,Integer,Unicode,ForeignKeyfromsqlalchemy.ext.declarativeimportdeclarative_baseBase=declarative_base()classAuthor(Base):__tablename__="author"id=Column(Integer,primary_key=True)name=Column(Unicode,nullable=False)classBook(Base):__tablename__="book"id=Column(Integer,primary_key=True)title=Column(Unicode,nullable=False)genre=Column(Unicode,nullable=False)author_id=Column(Integer,ForeignKey(Author.id))We then define object types for the root, books and authors:fromgraphqlimportGraphQLInt,GraphQLString,GraphQLArgumentfromgraphjoinerimportJoinType,RootJoinType,single,many,fieldfromsqlalchemy.ormimportQuerydefcreate_root():deffields():return{"books":many(book_join_type,books_query,args={"genre":GraphQLArgument(type=GraphQLString)})}defbooks_query(args,_):query=Query([]).select_from(Book)if"genre"inargs:query=query.filter(Book.genre==args["genre"])returnqueryreturnRootJoinType(name="Root",fields=fields)root=create_root()deffetch_immediates_from_database(selections,query,context):query=query.with_entities(*(selection.field.column_nameforselectioninselections))keys=tuple(selection.keyforselectioninselections)return[dict(zip(keys,row))forrowinquery.with_session(context.session).all()]defcreate_book_join_type():deffields():return{"id":field(column_name="id",type=GraphQLInt),"title":field(column_name="title",type=GraphQLString),"genre":field(column_name="genre",type=GraphQLString),"authorId":field(column_name="author_id",type=GraphQLInt),"author":single(author_join_type,author_query,join={"authorId":"id"}),}defauthor_query(args,book_query):books=book_query.with_entities(Book.author_id).distinct().subquery()returnQuery([])\.select_from(Author)\.join(books,books.c.author_id==Author.id)returnJoinType(name="Book",fields=fields,fetch_immediates=fetch_immediates_from_database,)book_join_type=create_book_join_type()defcreate_author_join_type():deffields():return{"id":field(column_name="id",type=GraphQLInt),"name":field(column_name="name",type=GraphQLString),}returnJoinType(name="Author",fields=fields,fetch_immediates=fetch_immediates_from_database,)author_join_type=create_author_join_type()We can execute the query by callingexecute:fromgraphjoinerimportexecutequery="""
    {
        books(genre: "comedy") {
            title
            author {
                name
            }
        }
    }
"""classContext(object):def__init__(self,session):self.session=sessionexecute(root,query,context=Context(session))Which produces:{
    "books": [
        {
            "title": "Leave It to Psmith",
            "author": {
                "name": "PG Wodehouse"
            }
        },
        {
            "title": "Right Ho, Jeeves",
            "author": {
                "name": "PG Wodehouse"
            }
        },
        {
            "title": "Catch-22",
            "author": {
                "name": "Joseph Heller"
            }
        },
    ]
}Let’s break things down a little, starting with the definition of the root object:defcreate_root():deffields():return{"books":many(book_join_type,books_query,args={"genre":GraphQLArgument(type=GraphQLString)})}defbooks_query(args,_):query=Query([]).select_from(Book)if"genre"inargs:query=query.filter(Book.genre==args["genre"])returnqueryreturnRootJoinType(name="Root",fields=fields)root=create_root()For each object type, we need to define its fields.
The root has only one field,books, a one-to-many relationship,
which we define usingmany().
The first argument,book_join_type,
is the type we’re defining a relationship to.
The second argument to describes how to create a query representing all of those
related books: in this case all books, potentially filtered by a genre argument.This means we need to definebook_join_type:defcreate_book_join_type():deffields():return{"id":field(column_name="id",type=GraphQLInt),"title":field(column_name="title",type=GraphQLString),"genre":field(column_name="genre",type=GraphQLString),"authorId":field(column_name="author_id",type=GraphQLInt),"author":single(author_join_type,author_query,join={"authorId":"id"}),}defauthor_query(args,book_query):books=book_query.with_entities(Book.author_id).distinct().subquery()returnQuery([])\.select_from(Author)\.join(books,books.c.author_id==Author.id)returnJoinType(name="Book",fields=fields,fetch_immediates=fetch_immediates_from_database,)book_join_type=create_book_join_type()Theauthorfield is defined as a one-to-one mapping from book to author.
As before, we define a function that generates a query for the requested authors.
We also provide ajoinargument tosingle()so that GraphJoiner knows
how to join together the results of the author query and the book query:
in this case, theauthorIdfield on books corresponds to theidfield
on authors.
(If we leave out thejoinargument, then GraphJoiner will perform a cross
join i.e. a cartesian product. Since there’s always exactly one root instance,
this is fine for relationships defined on the root.)The remaining fields define a mapping from the GraphQL field to the database
column. This mapping is handled byfetch_immediates_from_database.
The value ofselectionsinfetch_immediates()is the selections of fields that aren’t defined as relationships
(usingsingleormany) that were either explicitly requested in the
original GraphQL query, or are required as part of the join.deffetch_immediates_from_database(selections,query,context):query=query.with_entities(*(fields[selection.field_name].column_nameforselectioninselections))keys=tuple(selection.keyforselectioninselections)return[dict(zip(keys,row))forrowinquery.with_session(context.session).all()]For completeness, we can tweak the definition ofauthor_join_typeso
we can request the books by an author:defcreate_author_join_type():deffields():return{"id":field(column_name="id",type=GraphQLInt),"name":field(column_name="name",type=GraphQLString),"author":many(book_join_type,book_query,join={"id":"authorId"}),}defbook_query(args,author_query):authors=author_query.with_entities(Author.id).distinct().subquery()returnQuery([])\.select_from(Book)\.join(authors,authors.c.id==Book.author_id)returnJoinType(name="Author",fields=fields,fetch_immediates=fetch_immediates_from_database,)author_join_type=create_author_join_type()Installationpip install graphjoiner