Mastering Graph Relations
SurrealDB is a Multi-Model database, meaning it thrives as both a Document and a Graph database. While simple Field.record() links are great for one-to-many relations, Relation Tables (Edges) allow for powerful many-to-many structures with their own properties.
๐ Relation Tables (Edges)
Section titled โ๐ Relation Tables (Edges)โIn Unreal ORM, relation tables are defined using Table.relation(). They require two special fields: in and out.
Defining an Edge
Section titled โDefining an EdgeโSuppose we have User and Project models. We want to track which users are โmembersโ of which projects, and include their โroleโ on that specific project.
import Table, { Field } from 'unreal-orm';
class MemberOf extends Table.relation({ name: 'member_of', fields: { // The source (User) in: Field.record(() => User), // The target (Project) out: Field.record(() => Project), // Edge property role: Field.string({ default: surql`"contributor"` }), joinedAt: Field.datetime({ default: surql`time::now()` }), },}) {}Creating an Edge
Section titled โCreating an EdgeโTo create a relationship, you create an instance of the edge model just like any other record.
await MemberOf.create(db, { in: userId, out: projectId, role: 'admin',});๐ Traversing Relations
Section titled โ๐ Traversing RelationsโOne of the main benefits of using edges is the ability to traverse them easily in both directions.
Using fetch for hydration
Section titled โUsing fetch for hydrationโYou can hydrate the in or out records automatically during a select:
const memberships = await MemberOf.select(db, { where: surql`out = ${projectId}`, fetch: ['in'], // Hydrate the User record});
console.log(memberships[0].in.name); // "Alice"๐ Path Traversal in FROM
Section titled โ๐ Path Traversal in FROMโSurrealDB allows you to query directly from a relationship path. This is often faster and more efficient than using a WHERE clause because it uses graph indices to jump directly between records.
In Unreal ORM, you can modify the from option in select to use a graph path.
// Find all projects that Alice is a member of// This traverses: User (Alice) -> MemberOf Edge -> Projectconst aliceProjects = await Project.select(db, { from: surql`user:alice->member_of->project`,});๐ Advanced Projections with typed()
Section titled โ๐ Advanced Projections with typed()โWhen selecting related data, you can use the typed<T>() helper to ensure the results are correctly typed in TypeScript.
Projecting Relation Data
Section titled โProjecting Relation DataโYou can project data across relations directly in the select block.
import { typed } from 'unreal-orm';
const projects = await Project.select(db, { select: { name: true, // Project the count of members using a graph expression memberCount: typed<number>(surql`count(<-member_of)`), // Fetch the names of all members memberNames: typed<string[]>(surql`<-member_of<-user.name`), },});
// projects[0].memberCount is typed as number// projects[0].memberNames is typed as string[]๐ Official Resources
Section titled โ๐ Official ResourcesโFor a deep dive into how SurrealDB handles graph data, check out the official documentation:
- SurrealDB Graph Models โ Details on IDs, Edges, and Graph Traversal.
๐ก Many-to-Many vs. Record Links
Section titled โ๐ก Many-to-Many vs. Record Linksโ| Feature | Field.record (Link) | Table.relation (Edge) |
|---|---|---|
| Cardinality | One-to-One / One-to-Many | Many-to-Many |
| Properties | No (Link only) | โ Yes (Properties on the Edge) |
| Query Speed | Fast (Direct lookup) | Fast (Index-backed graph traversal) |
| Complexity | Simple | More setup (Extra table) |
Recommendation: Use record links for simple ownership (e.g., Post has one Author). Use relation tables when the relationship itself has data (e.g., User follows User with a since timestamp).