Modelling many-to-many table relationships¶
Table of contents
Introduction¶
A many-to-many relationship between two tables can be established by creating a table typically called as
bridge/junction/join table and adding foreign-key constraints from it to the original tables.
Say we have the following two tables in our database schema:
articles (
  id SERIAL PRIMARY KEY,
  title TEXT
  ...
)
tags (
  id SERIAL PRIMARY KEY,
  tag_value TEXT
  ...
)
These two tables are related via a many-to-many relationship. i.e:
- an articlecan have manytags
- a taghas manyarticles
Step 1: Set up a table relationship in the database¶
This many-to-many relationship can be established in the database by:
- Creating a bridge table called - article_tagwith the following structure:- article_tag ( id SERIAL PRIMARY KEY article_id INT tag_id INT UNIQUE (article_id, tag_id) ... ) - Note - If you can have multiple rows linking the same - articleand- tagin your model, you can skip the unique constraint on- (article_id, tag_id)
- Adding foreign key constraints in the - article_tagtable for:- the articlestable using thearticle_idandidcolumns of the tables respectively
- the tagstable using thetag_idandidcolumns of the tables respectively
 
- the 
The table article_tag sits between the two tables involved in the many-to-many relationship and captures possible
permutations of their association via the foreign keys.
Step 2: Set up GraphQL relationships¶
To access the nested objects via the GraphQL API, create the following relationships:
- Array relationship, article_tagsfromarticlestable usingarticle_tag :: article_id -> id
- Object relationship, tagfromarticle_tagtable usingtag_id -> tags :: id
- Array relationship, tag_articlesfromtagstable usingarticle_tag :: tag_id -> id
- Object relationship, articlefromarticle_tagtable usingarticle_id -> articles :: id
Query using many-to-many relationships¶
We can now:
- fetch a list of - articleswith their- tags:- query { articles { id title article_tags { tag { id tag_value } } } } query { articles { id title article_tags { tag { id tag_value } } } }{ "data": { "articles": [ { "id": 1, "title": "sit amet", "article_tags": [ { "tag": { "id": 1, "tag_value": "mystery" } }, { "tag": { "id": 2, "tag_value": "biography" } } ] }, { "id": 2, "title": "a nibh", "article_tags": [ { "tag": { "id": 2, "tag_value": "biography" } }, { "tag": { "id": 5, "tag_value": "technology" } } ] } ] } }
- fetch a list of - tagswith their- articles:- query { tags { id tag_value tag_articles { article { id title } } } } query { tags { id tag_value tag_articles { article { id title } } } }{ "data": { "tags": [ { "id": 1, "tag_value": "mystery", "tag_articles": [ { "article": { "id": 1, "title": "sit amet" } } ] }, { "id": 2, "tag_value": "biography", "tag_articles": [ { "article": { "id": 1, "title": "sit amet" } }, { "article": { "id": 2, "title": "a nibh" } } ] } ] } }
Insert using many-to-many relationships¶
We can now:
- insert an articlewithtagswhere thetagmight already exist (assume uniquevaluefortag):
mutation insertArticleWithTags {
  insert_article(objects: [
    {
      title: "Article 1",
      content: "Article 1 content",
      author_id: 1,
      article_tags: {
        data: [
          {
            tag: {
              data: {
                value: "Recipes"
              },
              on_conflict: {
                constraint: tag_value_key,
                update_columns: [value]
              }
            }
          }
          {
            tag: {
              data: {
                value: "Cooking"
              },
              on_conflict: {
                constraint: tag_value_key,
                update_columns: [value]
              }
            }
          }
        ]
      }
    }
  ]) {
    returning {
      title
      article_tags {
        tag {
          value
        }
      }
    }
  }
}
- insert a tagwitharticleswhere thetagmight already exist (assume uniquevaluefortag):
mutation insertTagWithArticles {
  insert_tag(objects: [
    {
      value: "Recipes",
      article_tags: {
        data: [
          {
            article: {
              data: {
                title: "Article 1",
                content: "Article 1 content",
                author_id: 1
              }
            }
          },
          {
            article: {
              data: {
                title: "Article 2",
                content: "Article 2 content",
                author_id: 1
              }
            }
          }
        ]
      }
    }
  ],
  on_conflict: {
    constraint: tag_value_key,
    update_columns: [value]
  }
  ) {
    returning {
      value
      article_tags {
        article {
          title
        }
      }
    }
  }
}
Note
You can avoid the on_conflict clause if you will never have conflicts.
Fetching relationship information¶
The intermediate fields article_tags & tag_articles can be used to fetch extra
information about the relationship. For example, you can have a column like tagged_at in the article_tag
table which you can fetch as follows:
query {
  articles {
    id
    title
    article_tags {
      tagged_at
      tag {
        id
        tag_value
      }
    }
  }
}
Flattening a many-to-many relationship query¶
In case you would like to flatten the above queries and avoid the intermediate fields article_tags &
tag_articles, you can create the following views additionally and then
query using relationships created on these views:
CREATE VIEW article_tags_view AS
  SELECT article_id, tags.*
    FROM article_tag LEFT JOIN tags
      ON article_tag.tag_id = tags.id
CREATE VIEW tag_articles_view AS
  SELECT tag_id, articles.*
    FROM article_tag LEFT JOIN articles
      ON article_tag.article_id = articles.id
Now create the following relationships:
- Array relationship, tagsfrom thearticlestable usingarticle_tags_view :: article_id -> id
- Array relationship, articlesfrom thetagstable usingtag_articles_view :: tag_id -> id
We can now:
- fetch articles with their tags without an intermediate field: - query { articles { id title tags { id tag_value } } } query { articles { id title tags { id tag_value } } }{ "data": { "articles": [ { "id": 1, "title": "sit amet", "tags": [ { "id": 1, "tag_value": "mystery" }, { "id": 3, "tag_value": "romance" } ] }, { "id": 2, "title": "a nibh", "tags": [ { "id": 5, "tag_value": "biography" }, { "id": 3, "tag_value": "romance" } ] } ] } }- fetch tags with their articles without an intermediate field: - query { tags { id tag_value articles { id title } } } query { tags { id tag_value articles { id title } } }{ "data": { "tags": [ { "id": 1, "tag_value": "mystery", "articles": [ { "id": 1, "title": "sit amet" } ] }, { "id": 2, "tag_value": "biography", "articles": [ { "id": 1, "title": "sit amet" }, { "id": 2, "title": "a nibh" } ] } ] } }
 
Note
We do not recommend this flattening pattern of modelling as this introduces an additional overhead of managing
permissions and relationships on the newly created views. e.g. You cannot query for the author of the nested articles
without setting up a new relationship to the authors table from the tag_articles_view view.
In our opinion, the cons of this approach seem to outweigh the pros.
