2020-01-14 15:57:45 +03:00
|
|
|
.. meta::
|
|
|
|
:description: Model many-to-many relationships in Hasura
|
|
|
|
:keywords: hasura, docs, schema, relationship, many-to-many, n-m
|
|
|
|
|
2019-12-26 15:05:37 +03:00
|
|
|
.. _many_to_many_modelling:
|
2019-11-28 17:50:47 +03:00
|
|
|
|
2018-11-06 11:38:40 +03:00
|
|
|
Modelling many-to-many table relationships
|
|
|
|
==========================================
|
|
|
|
|
2018-12-03 15:12:24 +03:00
|
|
|
.. contents:: Table of contents
|
|
|
|
:backlinks: none
|
|
|
|
:depth: 1
|
|
|
|
:local:
|
|
|
|
|
2020-07-08 00:47:42 +03:00
|
|
|
Introduction
|
|
|
|
------------
|
|
|
|
|
2018-11-23 12:29:03 +03:00
|
|
|
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.
|
2018-11-06 11:38:40 +03:00
|
|
|
|
|
|
|
Say we have the following two tables in our database schema:
|
|
|
|
|
|
|
|
.. code-block:: sql
|
|
|
|
|
2021-02-17 14:12:53 +03:00
|
|
|
articles (
|
2020-05-12 13:47:06 +03:00
|
|
|
id SERIAL PRIMARY KEY,
|
2018-11-06 11:38:40 +03:00
|
|
|
title TEXT
|
|
|
|
...
|
|
|
|
)
|
|
|
|
|
2021-02-17 14:12:53 +03:00
|
|
|
tags (
|
2020-05-12 13:47:06 +03:00
|
|
|
id SERIAL PRIMARY KEY,
|
2018-11-06 11:38:40 +03:00
|
|
|
tag_value TEXT
|
|
|
|
...
|
|
|
|
)
|
|
|
|
|
|
|
|
These two tables are related via a ``many-to-many`` relationship. i.e:
|
|
|
|
|
|
|
|
- an ``article`` can have many ``tags``
|
|
|
|
- a ``tag`` has many ``articles``
|
|
|
|
|
2020-07-08 00:47:42 +03:00
|
|
|
Step 1: Set up a table relationship in the database
|
|
|
|
---------------------------------------------------
|
2018-12-03 15:12:24 +03:00
|
|
|
|
2018-11-06 11:38:40 +03:00
|
|
|
This ``many-to-many`` relationship can be established in the database by:
|
|
|
|
|
2018-11-23 12:29:03 +03:00
|
|
|
1. Creating a **bridge table** called ``article_tag`` with the following structure:
|
2018-11-06 11:38:40 +03:00
|
|
|
|
|
|
|
.. code-block:: sql
|
|
|
|
|
|
|
|
article_tag (
|
|
|
|
article_id INT
|
|
|
|
tag_id INT
|
2019-09-04 10:08:32 +03:00
|
|
|
PRIMARY KEY (article_id, tag_id)
|
2018-11-06 11:38:40 +03:00
|
|
|
...
|
|
|
|
)
|
|
|
|
|
2019-09-11 10:17:14 +03:00
|
|
|
2. Adding **foreign key constraints** from the ``article_tag`` table to:
|
2018-11-06 11:38:40 +03:00
|
|
|
|
2021-02-17 14:12:53 +03:00
|
|
|
- the ``articles`` table using the ``article_id`` and ``id`` columns of the tables respectively
|
|
|
|
- the ``tags`` table using the ``tag_id`` and ``id`` columns of the tables respectively
|
2018-11-06 11:38:40 +03:00
|
|
|
|
|
|
|
|
|
|
|
The table ``article_tag`` sits between the two tables involved in the many-to-many relationship and captures possible
|
2019-09-11 10:17:14 +03:00
|
|
|
permutations of their association via the foreign keys.
|
2018-11-06 11:38:40 +03:00
|
|
|
|
2020-07-08 00:47:42 +03:00
|
|
|
Step 2: Set up GraphQL relationships
|
|
|
|
------------------------------------
|
2018-12-03 15:12:24 +03:00
|
|
|
|
2020-03-11 22:42:36 +03:00
|
|
|
To access the nested objects via the GraphQL API, :ref:`create the following relationships <create_relationships>`:
|
2018-11-06 11:38:40 +03:00
|
|
|
|
2021-02-17 14:12:53 +03:00
|
|
|
- Array relationship, ``article_tags`` from ``articles`` table using ``article_tag :: article_id -> id``
|
|
|
|
- Object relationship, ``tag`` from ``article_tag`` table using ``tag_id -> tags :: id``
|
|
|
|
- Array relationship, ``tag_articles`` from ``tags`` table using ``article_tag :: tag_id -> id``
|
|
|
|
- Object relationship, ``article`` from ``article_tag`` table using ``article_id -> articles :: id``
|
2018-11-06 11:38:40 +03:00
|
|
|
|
2021-02-02 11:44:36 +03:00
|
|
|
Query using many-to-many relationships
|
|
|
|
--------------------------------------
|
2018-11-06 11:38:40 +03:00
|
|
|
|
|
|
|
We can now:
|
|
|
|
|
2021-02-02 11:44:36 +03:00
|
|
|
- fetch a list of ``articles`` with their ``tags``:
|
2018-11-06 11:38:40 +03:00
|
|
|
|
|
|
|
.. graphiql::
|
|
|
|
:view_only:
|
|
|
|
:query:
|
|
|
|
query {
|
2021-02-17 14:12:53 +03:00
|
|
|
articles {
|
2018-11-06 11:38:40 +03:00
|
|
|
id
|
|
|
|
title
|
|
|
|
article_tags {
|
|
|
|
tag {
|
|
|
|
id
|
|
|
|
tag_value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
:response:
|
|
|
|
{
|
|
|
|
"data": {
|
2021-02-17 14:12:53 +03:00
|
|
|
"articles": [
|
2018-11-06 11:38:40 +03:00
|
|
|
{
|
|
|
|
"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"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-02 11:44:36 +03:00
|
|
|
- fetch a list of ``tags`` with their ``articles``:
|
2018-11-06 11:38:40 +03:00
|
|
|
|
|
|
|
.. graphiql::
|
|
|
|
:view_only:
|
|
|
|
:query:
|
|
|
|
query {
|
2021-02-17 14:12:53 +03:00
|
|
|
tags {
|
2018-11-06 11:38:40 +03:00
|
|
|
id
|
|
|
|
tag_value
|
|
|
|
tag_articles {
|
|
|
|
article {
|
|
|
|
id
|
|
|
|
title
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
:response:
|
|
|
|
{
|
|
|
|
"data": {
|
2021-02-17 14:12:53 +03:00
|
|
|
"tags": [
|
2018-11-06 11:38:40 +03:00
|
|
|
{
|
|
|
|
"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"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-02 11:44:36 +03:00
|
|
|
|
|
|
|
Insert using many-to-many relationships
|
|
|
|
---------------------------------------
|
|
|
|
|
|
|
|
We can now:
|
|
|
|
|
|
|
|
- insert an ``article`` with ``tags`` where the ``tag`` might already exist (assume unique ``value`` for ``tag``):
|
|
|
|
|
|
|
|
.. graphiql::
|
|
|
|
:view_only:
|
|
|
|
:query:
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
:response:
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"insert_article": {
|
|
|
|
"returning": [
|
|
|
|
{
|
|
|
|
"title": "Article 1",
|
|
|
|
"article_tags": [
|
|
|
|
{
|
|
|
|
"tag": {
|
|
|
|
"value": "Recipes"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"tag": {
|
|
|
|
"value": "Cooking"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- insert a ``tag`` with ``articles`` where the ``tag`` might already exist (assume unique ``value`` for ``tag``):
|
|
|
|
|
|
|
|
.. graphiql::
|
|
|
|
:view_only:
|
|
|
|
:query:
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
:response:
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"insert_tag": {
|
|
|
|
"returning": [
|
|
|
|
{
|
|
|
|
"value": "Recipes",
|
|
|
|
"article_tags": [
|
|
|
|
{
|
|
|
|
"article": {
|
|
|
|
"title": "Article 1"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"article": {
|
|
|
|
"title": "Article 2"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.. note::
|
|
|
|
|
|
|
|
You can avoid the ``on_conflict`` clause if you will never have conflicts.
|
|
|
|
|
2018-11-23 12:29:03 +03:00
|
|
|
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:
|
|
|
|
|
|
|
|
.. graphiql::
|
|
|
|
:view_only:
|
|
|
|
:query:
|
|
|
|
query {
|
2021-02-17 14:12:53 +03:00
|
|
|
articles {
|
2018-11-23 12:29:03 +03:00
|
|
|
id
|
|
|
|
title
|
|
|
|
article_tags {
|
|
|
|
tagged_at
|
|
|
|
tag {
|
|
|
|
id
|
|
|
|
tag_value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
:response:
|
|
|
|
{
|
|
|
|
"data": {
|
2021-02-17 14:12:53 +03:00
|
|
|
"articles": [
|
2018-11-23 12:29:03 +03:00
|
|
|
{
|
|
|
|
"id": 1,
|
|
|
|
"title": "sit amet",
|
|
|
|
"article_tags": [
|
|
|
|
{
|
|
|
|
"tagged_at": "2018-11-19T18:01:17.292828+05:30",
|
|
|
|
"tag": {
|
|
|
|
"id": 1,
|
|
|
|
"tag_value": "mystery"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"tagged_at": "2018-11-18T18:01:17.292828+05:30",
|
|
|
|
"tag": {
|
|
|
|
"id": 3,
|
|
|
|
"tag_value": "romance"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"id": 2,
|
|
|
|
"title": "a nibh",
|
|
|
|
"article_tags": [
|
|
|
|
{
|
|
|
|
"tagged_at": "2018-11-19T15:01:17.292828+05:30",
|
|
|
|
"tag": {
|
|
|
|
"id": 5,
|
|
|
|
"tag_value": "biography"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"tagged_at": "2018-11-16T14:01:17.292828+05:30",
|
|
|
|
"tag": {
|
|
|
|
"id": 3,
|
|
|
|
"tag_value": "romance"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-12-03 15:12:24 +03:00
|
|
|
Flattening a many-to-many relationship query
|
|
|
|
--------------------------------------------
|
2018-11-23 12:29:03 +03:00
|
|
|
|
|
|
|
In case you would like to flatten the above queries and avoid the intermediate fields ``article_tags`` &
|
2020-03-11 22:42:36 +03:00
|
|
|
``tag_articles``, you can :ref:`create the following views <custom_views>` additionally and then
|
2018-11-23 12:29:03 +03:00
|
|
|
query using relationships created on these views:
|
|
|
|
|
|
|
|
.. code-block:: sql
|
|
|
|
|
|
|
|
CREATE VIEW article_tags_view AS
|
2021-02-17 14:12:53 +03:00
|
|
|
SELECT article_id, tags.*
|
|
|
|
FROM article_tag LEFT JOIN tags
|
|
|
|
ON article_tag.tag_id = tags.id
|
2018-11-23 12:29:03 +03:00
|
|
|
|
|
|
|
CREATE VIEW tag_articles_view AS
|
2021-02-17 14:12:53 +03:00
|
|
|
SELECT tag_id, articles.*
|
|
|
|
FROM article_tag LEFT JOIN articles
|
|
|
|
ON article_tag.article_id = articles.id
|
2018-11-06 11:38:40 +03:00
|
|
|
|
2020-03-11 22:42:36 +03:00
|
|
|
Now :ref:`create the following relationships <create_relationships>`:
|
2018-11-06 11:38:40 +03:00
|
|
|
|
2021-02-17 14:12:53 +03:00
|
|
|
- Array relationship, ``tags`` from the ``articles`` table using ``article_tags_view :: article_id -> id``
|
|
|
|
- Array relationship, ``articles`` from the ``tags`` table using ``tag_articles_view :: tag_id -> id``
|
2018-11-06 11:38:40 +03:00
|
|
|
|
2018-11-23 12:29:03 +03:00
|
|
|
We can now:
|
|
|
|
|
|
|
|
- fetch articles with their tags without an intermediate field:
|
|
|
|
|
|
|
|
.. graphiql::
|
|
|
|
:view_only:
|
|
|
|
:query:
|
2018-11-06 11:38:40 +03:00
|
|
|
query {
|
2021-02-17 14:12:53 +03:00
|
|
|
articles {
|
2018-11-06 11:38:40 +03:00
|
|
|
id
|
|
|
|
title
|
2018-11-23 12:29:03 +03:00
|
|
|
tags {
|
|
|
|
id
|
|
|
|
tag_value
|
2018-11-06 11:38:40 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-11-23 12:29:03 +03:00
|
|
|
:response:
|
|
|
|
{
|
|
|
|
"data": {
|
2021-02-17 14:12:53 +03:00
|
|
|
"articles": [
|
2018-11-23 12:29:03 +03:00
|
|
|
{
|
|
|
|
"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:
|
|
|
|
|
|
|
|
.. graphiql::
|
|
|
|
:view_only:
|
|
|
|
:query:
|
|
|
|
query {
|
2021-02-17 14:12:53 +03:00
|
|
|
tags {
|
2018-11-23 12:29:03 +03:00
|
|
|
id
|
|
|
|
tag_value
|
|
|
|
articles {
|
|
|
|
id
|
|
|
|
title
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
:response:
|
|
|
|
{
|
|
|
|
"data": {
|
2021-02-17 14:12:53 +03:00
|
|
|
"tags": [
|
2018-11-23 12:29:03 +03:00
|
|
|
{
|
|
|
|
"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
|
2021-02-17 14:12:53 +03:00
|
|
|
without setting up a new relationship to the ``authors`` table from the ``tag_articles_view`` view.
|
2018-11-23 12:29:03 +03:00
|
|
|
|
|
|
|
In our opinion, the cons of this approach seem to outweigh the pros.
|