{"id":13499368,"url":"https://github.com/healx/python-graphjoiner","last_synced_at":"2025-03-29T04:31:12.003Z","repository":{"id":57435834,"uuid":"318257804","full_name":"healx/python-graphjoiner","owner":"healx","description":"Implementing GraphQL with joins to avoid the N+1 problem","archived":false,"fork":true,"pushed_at":"2019-01-24T23:05:08.000Z","size":297,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-03-17T17:51:43.055Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"mwilliamson/python-graphjoiner","license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/healx.png","metadata":{"files":{"readme":"README.rst","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-12-03T16:43:23.000Z","updated_at":"2024-06-14T22:48:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/healx/python-graphjoiner","commit_stats":null,"previous_names":[],"tags_count":42,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/healx%2Fpython-graphjoiner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/healx%2Fpython-graphjoiner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/healx%2Fpython-graphjoiner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/healx%2Fpython-graphjoiner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/healx","download_url":"https://codeload.github.com/healx/python-graphjoiner/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246140565,"owners_count":20729797,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T22:00:32.531Z","updated_at":"2025-03-29T04:31:11.553Z","avatar_url":"https://github.com/healx.png","language":null,"readme":"GraphJoiner: Implementing GraphQL with joins\n============================================\n\nGraphJoiner has been superseded by `GraphLayer`_,\nwhich uses the same fundamental ideas with a significantly simpler API.\n\n.. _GraphLayer: https://github.com/mwilliamson/python-graphlayer\n\nIn the reference GraphQL implementation, resolve functions describe how to\nfulfil some part of the requested data for each instance of an object.\nIf implemented naively with a SQL backend, this results in the N+1 problem.\nFor instance, given the query:\n\n::\n\n    {\n        books(genre: \"comedy\") {\n            title\n            author {\n                name\n            }\n        }\n    }\n\nA naive GraphQL implementation would issue one SQL query to get the list of all\nbooks in the comedy genre, and then N queries to get the author of each book\n(where N is the number of books returned by the first query).\n\nThere are various solutions proposed to this problem: GraphJoiner suggests that\nusing joins is a natural fit for many use cases. For this specific case, we only\nneed to run two queries: one to find the list of all books in the comedy genre,\nand one to get the authors of books in the comedy genre.\n\nInstallation\n------------\n\n::\n\n    pip install graphjoiner\n\nExample\n-------\n\nLet's say we have some models defined by SQLAlchemy. A book has an ID, a title,\na genre and an author ID. An author has an ID and a name.\n\n.. code-block:: python\n\n        from sqlalchemy import Column, Integer, Unicode, ForeignKey\n        from sqlalchemy.ext.declarative import declarative_base\n\n        Base = declarative_base()\n\n        class AuthorRecord(Base):\n            __tablename__ = \"author\"\n\n            id = Column(Integer, primary_key=True)\n            name = Column(Unicode, nullable=False)\n\n        class BookRecord(Base):\n            __tablename__ = \"book\"\n\n            id = Column(Integer, primary_key=True)\n            title = Column(Unicode, nullable=False)\n            genre = Column(Unicode, nullable=False)\n            author_id = Column(Integer, ForeignKey(AuthorRecord.id))\n\nWe then define object types for the root, books and authors:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import RootType, single, many, select, String\n    from graphjoiner.declarative.sqlalchemy import SqlAlchemyObjectType, column_field, sql_join\n\n    class Author(SqlAlchemyObjectType):\n        __model__ = AuthorRecord\n\n        id = column_field(AuthorRecord.id)\n        name = column_field(AuthorRecord.name)\n\n    class Book(SqlAlchemyObjectType):\n        __model__ = BookRecord\n\n        id = column_field(BookRecord.id)\n        title = column_field(BookRecord.title)\n        genre = column_field(BookRecord.genre)\n        author_id = column_field(BookRecord.author_id)\n        author = single(lambda: sql_join(Author))\n\n    class Root(RootType):\n        books = many(lambda: select(Book))\n\n        @books.arg(\"genre\", String)\n        def books_arg_genre(query, genre):\n            return query.filter(BookRecord.genre == genre)\n\nWe create an ``execute()`` function by calling ``executor()`` with our ``Root``:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import executor\n\n    execute = executor(Root)\n\n``execute`` can then be used to execute queries:\n\n.. code-block:: python\n\n    query = \"\"\"\n        {\n            books(genre: \"comedy\") {\n                title\n                author {\n                    name\n                }\n            }\n        }\n    \"\"\"\n\n    class Context(object):\n        def __init__(self, session):\n            self.session = session\n\n    result = execute(root, query, context=Context(session))\n\n\nWhere ``result.data`` is:\n\n::\n\n    {\n        \"books\": [\n            {\n                \"title\": \"Leave It to Psmith\",\n                \"author\": {\n                    \"name\": \"PG Wodehouse\"\n                }\n            },\n            {\n                \"title\": \"Right Ho, Jeeves\",\n                \"author\": {\n                    \"name\": \"PG Wodehouse\"\n                }\n            },\n            {\n                \"title\": \"Catch-22\",\n                \"author\": {\n                    \"name\": \"Joseph Heller\"\n                }\n            },\n        ]\n    }\n\nLet's break things down a little, starting with the definition of ``Author``:\n\n.. code-block:: python\n\n    class Author(SqlAlchemyObjectType):\n        __model__ = AuthorRecord\n\n        id = column_field(AuthorRecord.id)\n        name = column_field(AuthorRecord.name)\n\nWhen defining object types that represent SQLAlchemy models,\nwe can inherit from ``SqlAlchemyObjectType``,\nwith the ``__model__`` attribute set to the appropriate model.\n\nFields that can be fetched without further joining can be defined using ``column_field()``.\nGraphJoiner will automatically infer the GraphQL type of the field based on the SQL type of the column.\n\nNext is the definition of ``Book``:\n\n.. code-block:: python\n\n    class Book(SqlAlchemyObjectType):\n        __model__ = BookRecord\n\n        id = column_field(BookRecord.id)\n        title = column_field(BookRecord.title)\n        genre = column_field(BookRecord.genre)\n        author_id = column_field(BookRecord.author_id)\n        author = single(lambda: sql_join(Author))\n\nAs before, we inherit from ``SqlAlchemyObjectType``,\nset ``__model__`` to the appropriate class,\nand define a number of fields that correspond to columns.\n\nWe also define an ``author`` field that allows a book to be joined to an author.\nGraphJoiner will automatically inspect ``BookRecord`` and ``AuthorRecord``\nand use the foreign keys to determine how they should be joined together.\nTo override this behaviour, you can pass in an explicit ``join`` argument:\n\n.. code-block:: python\n\n    author = single(lambda: sql_join(Author, join={Book.author_id: Author.id}))\n\nThis explicitly tells GraphJoiner that authors can be joined to books\nby equality between the fields ``Book.author_id`` and ``Author.id``.\nWhen defining relationships such as this,\nwe call ``single()`` with a lambda to defer evaluation until all of the types and fields have been defined.\n\nFinally, we can create a root object:\n\n.. code-block:: python\n\n    class Root(RootType):\n        books = many(lambda: select(Book))\n\n        @books.arg(\"genre\", String)\n        def books_arg_genre(query, genre):\n            return query.filter(BookRecord.genre == genre)\n\nThe root has only one field, ``books``, which we define using ``many()``.\nUsing ``select`` tells GraphJoiner to select all of the books in the database,\nrather than trying to perform a join.\n\nUsing ``books.arg()`` adds an optional argument to the field.\n\nFor completeness, we can tweak the definition of ``Author`` so\nwe can request the books by an author:\n\n.. code-block:: python\n\n    class Author(SqlAlchemyObjectType):\n        __model__ = AuthorRecord\n\n        id = column_field(AuthorRecord.id)\n        name = column_field(AuthorRecord.name)\n        books = many(lambda: sql_join(Book))\n\n\nAPI\n---\n\n``graphjoiner.declarative``\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nObjectType\n^^^^^^^^^^\n\nRepresents a GraphQL object type.\nFields can be declared as attributes.\nFor instance, to create an object type called ``User`` with a ``name`` and ``emailAddress`` field:\n\n.. code-block:: python\n\n    from graphqjoiner import NonNull, ObjectType, String\n\n    class User(ObjectType):\n        name = NonNull(String)\n        email_address = NonNull(String)\n\nField names are inferred from attribute names,\nconverting from snake case to camel case.\nIn the example above, the attribute name ``email_address`` is converted to the field name ``emailAddress``.\n\nTo create a type that can be joined to,\nimplement ``__fetch_immediates__`` as a static or class method.\n\n* ``__fetch_immediates__(selections, query, context)``:\n  fetch the values for the selected fields that aren't defined as relationships.\n\n  Receives the arguments:\n\n  * ``selections``: an iterable of the selections,\n    where each selection has the attributes:\n\n    * ``field``: the field being selected\n    * ``args``: the arguments for the selection\n    * ``selections``: the sub-selections of that selection\n\n  * ``query``: the query for the records to select.\n\n  * ``context``: the context as passed into the executor\n\n  Should return a list of tuples,\n  where each tuple contains the value for each selection in the same order.\n\nImplementing ``__select_all__`` allows the object to be used with ``select()``.\n``__select_all__()`` takes no arguments,\nand should return a query that represents all instances of the object.\n\nFor instance,\nto implement a base type for static data:\n\n.. code-block:: python\n\n    import collections\n\n    from graphjoiner.declarative import ObjectType, RootType, select, single, String\n\n    class StaticDataObjectType(ObjectType):\n        @classmethod\n        def __select_all__(cls):\n            return cls.__records__\n\n        @classmethod\n        def __fetch_immediates__(cls, selections, records, context):\n            return [\n                tuple(\n                    getattr(record, selection.field.attr_name)\n                    for selection in selections\n                )\n                for record in records\n            ]\n\n    AuthorRecord = collections.namedtuple(\"AuthorRecord\", [\"name\"])\n\n    class Author(StaticDataObjectType):\n        __records__ = [AuthorRecord(\"PG Wodehouse\")]\n\n        name = field(type=String)\n\n    class Root(RootType):\n        author = single(lambda: select(Author))\n\n\nRelationships\n^^^^^^^^^^^^^\n\nUse ``single``, ``single_or_null``, ``first_or_null`` and ``many`` to create fields that are joined to other types.\nFor instance, to select all books from the root type:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import many, RootType, select\n\n    class Root(RootType):\n        ...\n        books = many(lambda: select(Book))\n\nEach relationship function accepts a joiner:\na value that describes how to join the left type to the right type.\nThe joiner is always wrapped in a lambda to defer evaluation until all types are defined.\nIn this case, the left type is ``Root``, the right type is ``Book``,\nand the joiner is ``select(Book)``.\nCalling ``select()`` with just the right type tells GraphJoiner to select all values,\nin this case all books.\n\nAll joiners accept a ``filter`` argument that allow the query to be tweaked.\nFor instance,\nsupposing books are selected using SQLAlchemy queries,\nand we want the ``books`` field to be sorted by title:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import many, RootType, select\n    from graphjoiner.declarative.sqlalchemy import SqlAlchemyObjectType\n\n    class Book(SqlAlchemyObjectType):\n        __model__ = BookRecord\n\n        ...\n\n        @staticmethod\n        def order_by_title(query):\n            # query is an instance of sqlalchemy.orm.Query\n            return query.order_by(BookRecord.title)\n\n    class Root(RootType):\n        ...\n\n        books = many(lambda: select(\n            Book,\n            filter=Book.order_by_title,\n        ))\n\nArguments can be added using the ``arg()`` decorator.\nIf the GraphQL selection for that field includes a value for the argument,\nthe query is updated using the decorated function.\nFor instance, to allow books to be filtered by title:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import many, RootType, select, String\n    from graphjoiner.declarative.sqlalchemy import SqlAlchemyObjectType\n\n    class Book(SqlAlchemyObjectType):\n        __model__ = BookRecord\n\n        ...\n\n        @staticmethod\n        def filter_by_title(query, title):\n            # query is an instance of sqlalchemy.orm.Query\n            return query.filter(BookRecord.title == title)\n\n    class Root(RootType):\n        ...\n\n        books = many(lambda: select(Book))\n        @books.arg(\"title\", String)\n        def books_arg_title(query, title):\n            return Book.filter_by_title(query, title)\n\n\n``select(target, join_query=None, join_fields=None)``\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nCreates a joiner to the target type.\nWhen given no additional arguments,\nit will select all values of the target type using ``target.__select_all__()``.\nAll left values are joined onto all right values\ni.e. the join is the cartesian product.\nUnless the left type is the root type,\nthis probably isn't what you want.\n\nSet ``join_fields`` to describe which fields to use to join together the left and right types.\nEach item in the dictionary should map a field from the left type to a field from the right type.\nFor instance, supposing each author has a unique ID,\nand each book has an author ID:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import field, Int, ObjectType, select, single\n\n    class Book(ObjectType):\n        ...\n        author_id = field(type=Int)\n        author = single(lambda: select(\n            Author,\n            join_fields={Book.author_id: Author.id},\n        ))\n\nSet ``join_query`` to describe how to join the left query and the right query.\nThis should be a function that accepts a left query and a right query,\nand returns a right query filtered to the values relevant to the left query.\nThis avoids the cost of fetching all values of the right type only to discard those that don't join onto any left values.\nFor instance, when using the ``sqlalchemy`` module,\nwe'd like to fetch the authors for just the requested book,\nrather than all available authors:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import select, single\n    from graphjoiner.declarative.sqlalchemy import column_field, SqlAlchemyObjectType\n\n    class Book(SqlAlchemyObjectType):\n        ...\n        author_id = column_field(BookRecord.author_id)\n\n        def join_authors(book_query, author_query):\n            author_ids = book_query \\\n                .add_columns(BookRecord.author_id) \\\n                .subquery()\n\n            return author_query.join(\n                author_ids,\n                author_ids.c.author_id == AuthorRecord.id,\n            )\n\n        author = single(lambda: select(\n            Author,\n            join_query=join_authors,\n            join_fields={Book.author_id: Author.id},\n        ))\n\nIn this particular case, using ``sql_join()`` would remove much of the boilerplate:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import single\n    from graphjoiner.declarative.sqlalchemy import column_field, sql_join, SqlAlchemyObjectType\n\n    class Book(SqlAlchemyObjectType):\n        ...\n        author_id = column_field(BookRecord.author_id)\n        author = single(lambda: sql_join(Author, {Book.author_id: Author.id}))\n\n``extract(field, sub_field)``\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nCreate a new field by extracting ``sub_field`` from ``field``.\nThe arguments for the new field are the same as the arguments for ``field``.\n\nFor instance,\nsupposing we have a field ``books`` on the root type,\neach book has a ``title`` field,\nand we want to add a ``bookTitles`` field to the root type:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import extract, many, RootType, select\n\n    class Root(RootType):\n        books = many(lambda: select(Book))\n        book_titles = extract(books, lambda: Book.title)\n\nIf we want to just have the ``bookTitles`` field without a ``books`` field,\nwe can pass the relationship directly into ``extract()``:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import extract, many, RootType, select\n\n    class Root(RootType):\n        book_titles = extract(\n            many(lambda: select(Book)),\n            lambda: Book.title,\n        )\n\n``extract()`` is often useful when modelling many-to-many relationships.\nFor instance,\nsuppose a book may have many publishers,\nand each publisher may publish many books.\nWe define a type that associates books and publishers:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import ObjectType, select, single\n\n    class BookPublisherAssociation(ObjectType):\n        book = single(lambda: select(Book, ...))\n        publisher = single(lambda: select(Publisher, ...))\n\nWe can then use ``extract`` to define a field for all publishers of a book,\nand a field for books from a publisher:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import extract, many, ObjectType, select\n\n    class Book(ObjectType):\n        ...\n        publishers = extract(\n            many(lambda: select(BookPublisherAssociation, ...)),\n            lambda: BookPublisherAssociation.publisher,\n        )\n\n    class Publisher(ObjectType):\n        ...\n        books = extract(\n            many(lambda: select(BookPublisherAssociation, ...)),\n            lambda: BookPublisherAssociation.book,\n        )\n\nInterfaces\n^^^^^^^^^^\n\nTo define an interface,\nsubclass ``InterfaceType`` and specify fields using ``field()``:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import InterfaceType, String\n\n    class HasName(InterfaceType):\n        name = field(type=String)\n\nTo set which interfaces an object implements,\nset the ``__interfaces__`` attribute:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import ObjectType\n\n    class Author(ObjectType):\n        __interfaces__ = lambda: [HasName]\n        ...\n\nField sets\n^^^^^^^^^^\n\nField sets can be used to define multiple fields using a single attribute.\nFor instance, this definition without field sets:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import field, Int, ObjectType, String\n\n    class Book(ObjectType):\n        title = field(type=String)\n        author_id = field(type=Int)\n\nis roughly equivalent to this definition using field sets:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import field, field_set, ObjectType, String\n\n    class Book(ObjectType):\n        fields = field_set(\n            title=field(type=String),\n            author_id=field(type=String),\n        )\n\nField sets are useful when a set of fields needs to be generated dynamically.\n\nInput object types\n^^^^^^^^^^^^^^^^^^\n\nDefine input types by inheriting from ``InputObjectType``,\nand defining fields using ``field()``.\nFor instance:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import InputObjectType, String\n\n    class BookSelectionInput(InputObjectType):\n        title = field(type=String, default=None)\n\nThe fields on input object values are available as attributes.\nFor instance:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import many, RootType, select, String\n    from graphjoiner.declarative.sqlalchemy import SqlAlchemyObjectType\n\n    class Book(SqlAlchemyObjectType):\n        __model__ = BookRecord\n\n        ...\n\n        @staticmethod\n        def filter_by_title(query, title):\n            # query is an instance of sqlalchemy.orm.Query\n            return query.filter(BookRecord.title == title)\n\n    class Root(RootType):\n        ...\n\n        books = many(lambda: select(Book))\n        @books.arg(\"selection\", BookSelectionInput)\n        def books_arg_title(query, title):\n            if selection.title is not None:\n                query = Book.filter_by_title(query, title)\n\n            return query\n\nThe default value for each field can be set by passing the ``default`` argument to each field.\nTo allow the absence of a value to be distinguished from an explicit null value,\nthe default value for a field is ``undefined`` if the ``default`` argument is not set.\nFor instance,\nto allow books to be filtered by title,\nincluding null titles:\n\n.. code-block:: python\n\n    from graphjoiner.declarative import field, InputObjectType, many, RootType, select, String, undefined\n    from graphjoiner.declarative.sqlalchemy import SqlAlchemyObjectType\n\n    class BookSelectionInput(InputObjectType):\n        title = field(type=String)\n\n    class Book(SqlAlchemyObjectType):\n        __model__ = BookRecord\n\n        ...\n\n        @staticmethod\n        def filter_by_title(query, title):\n            # query is an instance of sqlalchemy.orm.Query\n            return query.filter(BookRecord.title == title)\n\n    class Root(RootType):\n        ...\n\n        books = many(lambda: select(Book))\n        @books.arg(\"selection\", BookSelectionInput)\n        def books_arg_title(query, title):\n            if selection.title is not undefined:\n                query = Book.filter_by_title(query, title)\n\n            return query\n\nCore Example\n------------\n\nThe declarative API of GraphJoiner is built on top of a core API.\nThe core API exposes the fundamentals of how GraphJoiner works,\ngiving greater flexibility at the cost of being rather verbose to use directly.\nThe below shows how the original example could be written using the core API.\nIn general,\nusing the declarative API should be preferred,\neither by using the built-in tools or adding your own.\n\nLet's say we have some models defined by SQLAlchemy. A book has an ID, a title,\na genre and an author ID. An author has an ID and a name.\n\n.. code-block:: python\n\n    from sqlalchemy import Column, Integer, Unicode, ForeignKey\n    from sqlalchemy.ext.declarative import declarative_base\n\n    Base = declarative_base()\n\n    class Author(Base):\n        __tablename__ = \"author\"\n\n        id = Column(Integer, primary_key=True)\n        name = Column(Unicode, nullable=False)\n\n    class Book(Base):\n        __tablename__ = \"book\"\n\n        id = Column(Integer, primary_key=True)\n        title = Column(Unicode, nullable=False)\n        genre = Column(Unicode, nullable=False)\n        author_id = Column(Integer, ForeignKey(Author.id))\n\nWe then define object types for the root, books and authors:\n\n.. code-block:: python\n\n    from graphql import GraphQLInt, GraphQLString, GraphQLArgument\n    from graphjoiner import JoinType, RootJoinType, single, many, field\n    from sqlalchemy.orm import Query\n\n    def create_root():\n        def fields():\n            return {\n                \"books\": many(\n                    book_join_type,\n                    books_query,\n                    args={\"genre\": GraphQLArgument(type=GraphQLString)}\n                )\n            }\n\n        def books_query(args, _):\n            query = Query([]).select_from(Book)\n\n            if \"genre\" in args:\n                query = query.filter(Book.genre == args[\"genre\"])\n\n            return query\n\n        return RootJoinType(name=\"Root\", fields=fields)\n\n    root = create_root()\n\n    def fetch_immediates_from_database(selections, query, context):\n        query = query.with_entities(*(\n            selection.field.column_name\n            for selection in selections\n        ))\n\n        return query.with_session(context.session).all()\n\n    def create_book_join_type():\n        def fields():\n            return {\n                \"id\": field(column_name=\"id\", type=GraphQLInt),\n                \"title\": field(column_name=\"title\", type=GraphQLString),\n                \"genre\": field(column_name=\"genre\", type=GraphQLString),\n                \"authorId\": field(column_name=\"author_id\", type=GraphQLInt),\n                \"author\": single(author_join_type, author_query, join={\"authorId\": \"id\"}),\n            }\n\n        def author_query(args, book_query):\n            books = book_query.with_entities(Book.author_id).distinct().subquery()\n            return Query([]) \\\n                .select_from(Author) \\\n                .join(books, books.c.author_id == Author.id)\n\n        return JoinType(\n            name=\"Book\",\n            fields=fields,\n            fetch_immediates=fetch_immediates_from_database,\n        )\n\n    book_join_type = create_book_join_type()\n\n    def create_author_join_type():\n        def fields():\n            return {\n                \"id\": field(column_name=\"id\", type=GraphQLInt),\n                \"name\": field(column_name=\"name\", type=GraphQLString),\n            }\n\n        return JoinType(\n            name=\"Author\",\n            fields=fields,\n            fetch_immediates=fetch_immediates_from_database,\n        )\n    author_join_type = create_author_join_type()\n\nWe can execute the query by calling ``execute``:\n\n.. code-block:: python\n\n    from graphjoiner import execute\n\n    query = \"\"\"\n        {\n            books(genre: \"comedy\") {\n                title\n                author {\n                    name\n                }\n            }\n        }\n    \"\"\"\n\n    class Context(object):\n        def __init__(self, session):\n            self.session = session\n\n    execute(root, query, context=Context(session))\n\n\nWhich produces:\n\n::\n\n    {\n        \"books\": [\n            {\n                \"title\": \"Leave It to Psmith\",\n                \"author\": {\n                    \"name\": \"PG Wodehouse\"\n                }\n            },\n            {\n                \"title\": \"Right Ho, Jeeves\",\n                \"author\": {\n                    \"name\": \"PG Wodehouse\"\n                }\n            },\n            {\n                \"title\": \"Catch-22\",\n                \"author\": {\n                    \"name\": \"Joseph Heller\"\n                }\n            },\n        ]\n    }\n\nLet's break things down a little, starting with the definition of the root object:\n\n.. code-block:: python\n\n    def create_root():\n        def fields():\n            return {\n                \"books\": many(\n                    book_join_type,\n                    books_query,\n                    args={\"genre\": GraphQLArgument(type=GraphQLString)}\n                )\n            }\n\n        def books_query(args, _):\n            query = Query([]).select_from(Book)\n\n            if \"genre\" in args:\n                query = query.filter(Book.genre == args[\"genre\"])\n\n            return query\n\n        return RootJoinType(name=\"Root\", fields=fields)\n\n    root = create_root()\n\nFor each object type, we need to define its fields.\nThe root has only one field, ``books``, a one-to-many relationship,\nwhich we define using ``many()``.\nThe first argument, ``book_join_type``,\nis the type we're defining a relationship to.\nThe second argument to describes how to create a query representing all of those\nrelated books: in this case all books, potentially filtered by a genre argument.\n\nThis means we need to define ``book_join_type``:\n\n.. code-block:: python\n\n    def create_book_join_type():\n        def fields():\n            return {\n                \"id\": field(column_name=\"id\", type=GraphQLInt),\n                \"title\": field(column_name=\"title\", type=GraphQLString),\n                \"genre\": field(column_name=\"genre\", type=GraphQLString),\n                \"authorId\": field(column_name=\"author_id\", type=GraphQLInt),\n                \"author\": single(author_join_type, author_query, join={\"authorId\": \"id\"}),\n            }\n\n        def author_query(args, book_query):\n            books = book_query.with_entities(Book.author_id).distinct().subquery()\n            return Query([]) \\\n                .select_from(Author) \\\n                .join(books, books.c.author_id == Author.id)\n\n        return JoinType(\n            name=\"Book\",\n            fields=fields,\n            fetch_immediates=fetch_immediates_from_database,\n        )\n\n    book_join_type = create_book_join_type()\n\nThe ``author`` field is defined as a one-to-one mapping from book to author.\nAs before, we define a function that generates a query for the requested authors.\nWe also provide a ``join`` argument to ``single()`` so that GraphJoiner knows\nhow to join together the results of the author query and the book query:\nin this case, the ``authorId`` field on books corresponds to the ``id`` field\non authors.\n(If we leave out the ``join`` argument, then GraphJoiner will perform a cross\njoin i.e. a cartesian product. Since there's always exactly one root instance,\nthis is fine for relationships defined on the root.)\n\nThe remaining fields define a mapping from the GraphQL field to the database\ncolumn. This mapping is handled by ``fetch_immediates_from_database``.\nThe value of ``selections`` in\n``fetch_immediates()`` is the selections of fields that aren't defined as relationships\n(using ``single`` or ``many``) that were either explicitly requested in the\noriginal GraphQL query, or are required as part of the join.\n\n.. code-block:: python\n\n    def fetch_immediates_from_database(selections, query, context):\n        query = query.with_entities(*(\n            fields[selection.field_name].column_name\n            for selection in selections\n        ))\n\n        return query.with_session(context.session).all()\n\nFor completeness, we can tweak the definition of ``author_join_type`` so\nwe can request the books by an author:\n\n.. code-block:: python\n\n    def create_author_join_type():\n        def fields():\n            return {\n                \"id\": field(column_name=\"id\", type=GraphQLInt),\n                \"name\": field(column_name=\"name\", type=GraphQLString),\n                \"author\": many(book_join_type, book_query, join={\"id\": \"authorId\"}),\n            }\n\n        def book_query(args, author_query):\n            authors = author_query.with_entities(Author.id).distinct().subquery()\n            return Query([]) \\\n                .select_from(Book) \\\n                .join(authors, authors.c.id == Book.author_id)\n\n        return JoinType(\n            name=\"Author\",\n            fields=fields,\n            fetch_immediates=fetch_immediates_from_database,\n        )\n\n    author_join_type = create_author_join_type()\n\n","funding_links":[],"categories":["Libraries","Implementations"],"sub_categories":["Python Libraries","Python"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhealx%2Fpython-graphjoiner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhealx%2Fpython-graphjoiner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhealx%2Fpython-graphjoiner/lists"}