Onyx Database Relationships

Many-to-Many Relationship Tutorial

The many-to-many relationship captures associations where multiple instances of one entity correspond to multiple instances of another. Use this guide to compare the ORM workflow in Onyx Open Source with the resolver-driven pattern in Onyx Cloud.

Switch between Onyx Open Source and Onyx Cloud to view the relevant steps and code examples for your runtime.

Onyx Open Source Tutorial

Switch to Onyx Open Source to review the ORM walkthrough and annotated entity snippets.

Onyx Cloud Tutorial

Model the relationship with resolver-backed attributes that read from a dedicated join table. Persist both the base records and join rows, then hydrate collections on demand with resolve().

1

Capture the schema revision

Declare the Movie, Actor, and MovieCast tables in JSON. Attach resolvers named after the collections you want to expose so queries remain consistent across the API and SDK.
1{
2  "tables": [
3    {
4      "name": "Movie",
5      "identifier": {
6        "name": "movieId",
7        "type": "String",
8        "generator": "None"
9      },
10      "attributes": [
11        {
12          "name": "movieId",
13          "type": "String"
14        },
15        {
16          "name": "title",
17          "type": "String"
18        }
19      ],
20      "indexes": [],
21      "resolvers": [
22        {
23          "name": "actors",
24          "resolver": "const castLinks = await db\n  .from("MovieCast")\n  .where(eq("movieId", this.movieId))\n  .list();\n\nif (castLinks.length === 0) {\n  return [];\n}\n\nconst actors = await db\n  .from("Actor")\n  .where(inOp("actorId", castLinks.map((link) => link.actorId)))\n  .list();\n\nreturn actors;"
25        }
26      ],
27      "triggers": []
28    },
29    {
30      "name": "Actor",
31      "identifier": {
32        "name": "actorId",
33        "type": "String",
34        "generator": "None"
35      },
36      "attributes": [
37        {
38          "name": "actorId",
39          "type": "String"
40        },
41        {
42          "name": "actorFirstName",
43          "type": "String"
44        },
45        {
46          "name": "actorLastName",
47          "type": "String"
48        }
49      ],
50      "indexes": [],
51      "resolvers": [
52        {
53          "name": "movies",
54          "resolver": "const castLinks = await db\n  .from("MovieCast")\n  .where(eq("actorId", this.actorId))\n  .list();\n\nif (castLinks.length === 0) {\n  return [];\n}\n\nconst movies = await db\n  .from("Movie")\n  .where(inOp("movieId", castLinks.map((link) => link.movieId)))\n  .list();\n\nreturn movies;"
55        }
56      ],
57      "triggers": []
58    },
59    {
60      "name": "MovieCast",
61      "identifier": {
62        "name": "id",
63        "type": "String",
64        "generator": "None"
65      },
66      "attributes": [
67        {
68          "name": "id",
69          "type": "String"
70        },
71        {
72          "name": "movieId",
73          "type": "String"
74        },
75        {
76          "name": "actorId",
77          "type": "String"
78        },
79        {
80          "name": "role",
81          "type": "String"
82        }
83      ],
84      "indexes": [],
85      "resolvers": [],
86      "triggers": []
87    }
88  ],
89  "revisionDescription": "Initial schema for many-to-many relationship example"
90}
  • Include both movieId and actorId columns on the join table to filter efficiently inside resolvers.
  • Return arrays from to-many resolvers and leverage inOp() to fetch the related side in a single round trip.
2

Persist movies, actors, and join rows

Create deterministic identifiers for each record, save the core tables, and then insert the MovieCast rows that represent the relationship.
1// Define Harrison Ford Actor
2const harrisonFordActor = new Actor();
3harrisonFordActor.actorFirstName = "Harrison";
4harrisonFordActor.actorLastName = "Ford";
5harrisonFordActor.actorId = "actor_harrisonFord";
6
7// Define Mark Hamill Actor
8const markHamillActor = new Actor();
9markHamillActor.actorFirstName = "Mark";
10markHamillActor.actorLastName = "Hamill";
11markHamillActor.actorId = "actor_markHamill";
12
13// Create Star Wars Movie
14const starWarsMovie = new Movie();
15starWarsMovie.title = "A New Hope";
16starWarsMovie.movieId = "movie_starWars";
17
18// Create Indiana Jones Movie
19const indianaJonesMovie = new Movie();
20indianaJonesMovie.title = "Raiders of the Lost Ark";
21indianaJonesMovie.movieId = "movie_indianaJones";
22
23// Persist the core records
24await db.save(starWarsMovie);
25await db.save(indianaJonesMovie);
26await db.save(harrisonFordActor);
27await db.save(markHamillActor);
28
29// Create the join rows that link movies and actors
30const starWarsHarrison = new MovieCast();
31starWarsHarrison.id = "cast_001";
32starWarsHarrison.movieId = starWarsMovie.movieId;
33starWarsHarrison.actorId = harrisonFordActor.actorId;
34starWarsHarrison.role = "Han Solo";
35
36const starWarsMark = new MovieCast();
37starWarsMark.id = "cast_002";
38starWarsMark.movieId = starWarsMovie.movieId;
39starWarsMark.actorId = markHamillActor.actorId;
40starWarsMark.role = "Luke Skywalker";
41
42const indianaJonesHarrison = new MovieCast();
43indianaJonesHarrison.id = "cast_003";
44indianaJonesHarrison.movieId = indianaJonesMovie.movieId;
45indianaJonesHarrison.actorId = harrisonFordActor.actorId;
46indianaJonesHarrison.role = "Indiana Jones";
47
48await db.save(starWarsHarrison);
49await db.save(starWarsMark);
50await db.save(indianaJonesHarrison);
  • Resolver-backed attributes are derived; persist the join rows explicitly to make the association durable.
  • Stable IDs simplify testing and keep the example output predictable—use sequences or UUIDs in production.
3

Query through the resolvers

Resolve the actors collection when fetching a movie and hydrate movies when retrieving an actor to verify both sides of the relationship.
1// Fetch a movie with its cast hydrated via the resolver
2const savedMovie = await db
3  .from("Movie")
4  .resolve("actors")
5  .where(eq("movieId", "movie_starWars"))
6  .firstOrNull();
7
8console.log(`${savedMovie?.title} features ${savedMovie?.actors?.length ?? 0} actors`);
9
10// Fetch an actor and hydrate the inverse resolver
11const savedActor = await db
12  .from("Actor")
13  .resolve("movies")
14  .where(eq("actorId", "actor_harrisonFord"))
15  .firstOrNull();
16
17console.log(`${savedActor?.actorFirstName} has appeared in ${savedActor?.movies?.length ?? 0} movies`);
  • Combine resolve() with predicates like where(eq('movieId', ...)) to scope lookups.
  • The same resolver names work with REST responses, GraphQL, and the JavaScript SDK.

Important Notes

Onyx Open Source

  • Cascade behaviour, not parent/child semantics, controls which entities persist together—pick the policy that matches your workflow.
  • Defining inverse collections is optional at runtime; the ORM infers them from your annotations after persistence.
  • Double-check inverseClass and inverse values to avoid runtime mapping errors.

Onyx Cloud Database

  • Resolver attributes expose many-to-many data by reading from join tables—keep resolver names consistent with the properties your clients consume.
  • Join tables remain the source of truth; resolvers simply shape read models for the API and SDK.
  • Use batching helpers like inOp() inside resolvers to minimize round trips when hydrating related IDs.

Troubleshooting

Onyx Open Source

  • Missing or invalid inverse: Ensure the inverse property matches the related field name and the inverse class references the correct entity.
  • Duplicate naming: Keep table, attribute, and relationship names unique to avoid schema validation errors.
  • Relationship mismatch: Pair many-to-many relationships on both sides and verify cascade/fetch policies are valid.

Onyx Cloud Database

  • Empty resolver results: Confirm join rows exist and that resolver filters query the correct identifiers.
  • Schema drift: Update resolver logic after renaming IDs or columns so joins still align with the schema.
  • Missing hydration: Include resolver names in resolve() calls or API query parameters whenever you need related collections.

Need Help?

If you have any questions or need assistance: