Back to Payload

Join Field

docs/fields/join.mdx

3.84.116.2 KB
Original Source

The Join Field is used to make Relationship and Upload fields available in the opposite direction. With a Join you can edit and view collections having reference to a specific collection document. The field itself acts as a virtual field, in that no new data is stored on the collection with a Join field. Instead, the Admin UI surfaces the related documents for a better editing experience and is surfaced by Payload's APIs.

The Join field is useful in scenarios including:

  • To surface Orders for a given Product
  • To view and edit Posts belonging to a Category
  • To work with any bi-directional relationship data
  • Displaying where a document or upload is used in other documents

<LightDarkImage srcLight="https://payloadcms.com/images/docs/fields/join.png" srcDark="https://payloadcms.com/images/docs/fields/join-dark.png" alt="Shows Join field in the Payload Admin Panel" caption="Admin Panel screenshot of Join field" />

For the Join field to work, you must have an existing relationship or upload field in the collection you are joining. This will reference the collection and path of the field of the related documents. To add a Relationship Field, set the type to join in your Field Config:

ts
import type { Field } from 'payload'

export const MyJoinField: Field = {
  // highlight-start
  name: 'relatedPosts',
  type: 'join',
  collection: 'posts',
  on: 'category',
  // highlight-end
}

// relationship field in another collection:
export const MyRelationshipField: Field = {
  name: 'category',
  type: 'relationship',
  relationTo: 'categories',
}

In this example, the field is defined to show the related posts when added to a category collection. The on property is used to specify the relationship field name of the field that relates to the collection document.

With this example, if you navigate to a Category in the Admin UI or an API response, you'll now see that the Posts which are related to the Category are populated for you. This is extremely powerful and can be used to define a wide variety of relationship types in an easy manner.

<Banner type="success"> The Join field is extremely performant and does not add additional query overhead to your API responses until you add depth of 1 or above. It works in all database adapters. In MongoDB, we use **aggregations** to automatically join in related documents, and in relational databases, we use joins. </Banner> <Banner type="warning"> The Join Field is not supported in [DocumentDB](https://aws.amazon.com/documentdb/) and [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db), as we internally use MongoDB aggregations to query data for that field, which are limited there. This can be changed in the future. </Banner>

Schema advice

When modeling your database, you might come across many places where you'd like to feature bi-directional relationships. But here's an important consideration—you generally only want to store information about a given relationship in one place.

Let's take the Posts and Categories example. It makes sense to define which category a post belongs to while editing the post.

It would generally not be necessary to have a list of post IDs stored directly on the category as well, for a few reasons:

  • You want to have a "single source of truth" for relationships, and not worry about keeping two sources in sync with one another
  • If you have hundreds, thousands, or even millions of posts, you would not want to store all of those post IDs on a given category
  • Etc.

This is where the join field is especially powerful. With it, you only need to store the category_id on the post, and Payload will automatically join in related posts for you when you query for categories. The related category is only stored on the post itself - and is not duplicated on both sides. However, the join field is what enables bi-directional APIs and UI for you.

Using the Join field to have full control of your database schema

For typical polymorphic / many relationships, if you're using Postgres or SQLite, Payload will automatically create a posts_rels table, which acts as a junction table to store all of a given document's relationships.

However, this might not be appropriate for your use case if you'd like to have more control over your database architecture. You might not want to have that _rels table, and would prefer to maintain / control your own junction table design.

<Banner type="success"> With the Join field, you can control your own junction table design, and avoid Payload's automatic _rels table creation. </Banner>

The join field can be used in conjunction with any collection - and if you wanted to define your own "junction" collection, which, say, is called categories_posts and has a post_id and a category_id column, you can achieve complete control over the shape of that junction table.

You could go a step further and leverage the admin.hidden property of the categories_posts collection to hide the collection from appearing in the Admin UI navigation.

Specifying additional fields on relationships

Another very powerful use case of the join field is to be able to define "context" fields on your relationships. Let's say that you have Posts and Categories, and use join fields on both your Posts and Categories collection to join in related docs from a new pseudo-junction collection called categories_posts. Now, the relations are stored in this third junction collection, and can be surfaced on both Posts and Categories. But, importantly, you could add additional "context" fields to this shared junction collection.

For example, on this categories_posts collection, in addition to having the category and post fields, we could add custom "context" fields like featured or spotlight, which would allow you to store additional information directly on relationships. The join field gives you complete control over any type of relational architecture in Payload, all wrapped up in a powerful Admin UI.

Config Options

OptionDescription
name *To be used as the property name when retrieved from the database. More details.
collection *The slugs having the relationship field or an array of collection slugs.
on *The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. If collection is an array, this field must exist for all specified collections
orderableIf true, enables custom ordering and joined documents can be reordered via drag and drop. Uses fractional indexing for efficient reordering.
whereA Where query to hide related documents from appearing. Will be merged with any where specified in the request.
maxDepthDefault is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. Max Depth.
labelText used as a field label in the Admin Panel or an object with keys for each language.
hooksProvide Field Hooks to control logic for this field. More details.
accessProvide Field Access Control to denote what users can see and do with this field's data. More details.
defaultLimitThe number of documents to return. Set to 0 to return all related documents.
defaultSortThe field name used to specify the order the joined documents are returned.
adminAdmin-specific configuration. More details.
customExtension point for adding custom data (e.g. for plugins).
typescriptSchemaOverride field type generation with providing a JSON schema.
graphQLCustom graphQL configuration for the field. More details

* An asterisk denotes that a property is required.

Admin Config Options

You can control the user experience of the join field using the admin config properties. The following options are supported:

OptionDescription
defaultColumnsArray of field names that correspond to which columns to show in the relationship table. Default is the collection config.
allowCreateSet to false to remove the controls for making new related documents from this field.
components.LabelOverride the default Label of the Field Component. More details
disableRowTypesSet to false to render row types, and true to hide them. Defaults to false for join fields with a singular relationTo, and true for join fields where relationTo is an array.

Join Field Data

When a document is returned that for a Join field is populated with related documents. The structure returned is an object with:

  • docs an array of related documents or only IDs if the depth is reached
  • hasNextPage a boolean indicating if there are additional documents
  • totalDocs a total number of documents, exists only if count: true is passed to the join query
json
{
  "id": "66e3431a3f23e684075aae9c",
  "relatedPosts": {
    "docs": [
      {
        "id": "66e3431a3f23e684075aaeb9",
        // other fields...
        "category": "66e3431a3f23e684075aae9c"
      }
      // { ... }
    ],
    "hasNextPage": false,
    "totalDocs": 10 // if count: true is passed
  }
  // other fields...
}

Join Field Data (polymorphic)

When a document is returned that for a polymorphic Join field (with collection as an array) is populated with related documents. The structure returned is an object with:

  • docs an array of relationTo - the collection slug of the document and value - the document itself or the ID if the depth is reached
  • hasNextPage a boolean indicating if there are additional documents
  • totalDocs a total number of documents, exists only if count: true is passed to the join query
json
{
  "id": "66e3431a3f23e684075aae9c",
  "relatedPosts": {
    "docs": [
      {
        "relationTo": "posts",
        "value": {
          "id": "66e3431a3f23e684075aaeb9",
          // other fields...
          "category": "66e3431a3f23e684075aae9c"
        }
      }
      // { ... }
    ],
    "hasNextPage": false,
    "totalDocs": 10 // if count: true is passed
  }
  // other fields...
}

Query Options

The Join Field supports custom queries to filter, sort, and limit the related documents that will be returned. In addition to the specific query options for each Join Field, you can pass joins: false to disable all Join Field from returning. This is useful for performance reasons when you don't need the related documents.

The following query options are supported:

PropertyDescription
limitThe maximum related documents to be returned, default is 10.
whereAn optional Where query to filter joined documents. Will be merged with the field where object.
sortA string used to order related results
countWhether include the count of related documents or not. Not included by default

These can be applied to the Local API, GraphQL, and REST API.

Local API

By adding joins to the Local API you can customize the request for each join field by the name of the field.

js
const result = await payload.find({
  collection: 'categories',
  where: {
    title: {
      equals: 'My Category',
    },
  },
  joins: {
    relatedPosts: {
      limit: 5,
      where: {
        title: {
          equals: 'My Post',
        },
      },
      sort: 'title',
    },
  },
})
<Banner type="warning"> Currently, `Where` query support on joined documents for join fields with an array of `collection` is limited and not supported for fields inside arrays and blocks. </Banner>

Rest API

The REST API supports the same query options as the Local API. You can use the joins query parameter to customize the request for each join field by the name of the field. For example, an API call to get a document with the related posts limited to 5 and sorted by title:

/api/categories/${id}?joins[relatedPosts][limit]=5&joins[relatedPosts][sort]=title

You can specify as many joins parameters as needed for the same or different join fields for a single request.

GraphQL

The GraphQL API supports the same query options as the local and REST APIs. You can specify the query options for each join field in your query.

Example:

graphql
query {
  Categories {
    docs {
      relatedPosts(
        sort: "createdAt"
        limit: 5
        where: { author: { equals: "66e3431a3f23e684075aaeb9" } }
        """
        Optionally pass count: true if you want to retrieve totalDocs
        """
        count: true -- s
      ) {
        docs {
          title
        }
        hasNextPage
        totalDocs
      }
    }
  }
}