Skip to content
bknd-define-relationship logo

Define Entity Relationships

bknd-define-relationship

Use when defining relationships between Bknd entities. Covers many-to-one, one-to-one, many-to-many, self-referencing relationships, junction tables, options like mappedBy and inversedBy, and UI vs code approaches.

SKILL.md

Full skill instructions

Define Entity Relationships

Create relationships between entities in Bknd (foreign keys, references, associations).

Prerequisites

  • At least two entities exist (see bknd-create-entity)
  • For code mode: Access to your schema file

Relationship Types

TypeUse CaseExample
Many-to-OneChild belongs to one parentPosts → User (author)
One-to-OneExclusive 1:1 pairingUser → Profile
Many-to-ManyBoth sides have multiplePosts ↔ Tags
Self-ReferencingEntity references itselfCategories → Parent Category

When to Use UI vs Code

Use UI Mode When

  • Quick prototyping
  • Visual learners
  • Non-developers setting up relationships

Use Code Mode When

  • Version control needed
  • Reproducible schema
  • Custom options (mappedBy, connectionTable)
  • Team collaboration

UI Approach

Step 1: Access Data Section

  1. Start server: npx bknd run
  2. Open http://localhost:1337
  3. Navigate to Data section

Step 2: Add Relation Field

  1. Click on the child entity (e.g., posts)
  2. Click + Add Field
  3. Select Relation field type
  4. Choose the target entity (e.g., users)
  5. Select relationship type:
    • Many-to-One: Multiple posts can belong to one user
    • One-to-One: One post has exactly one user
    • Many-to-Many: Posts can have many tags, tags can have many posts

Step 3: Configure Options

  • Field Name: Name for the foreign key (e.g., author creates author_id)
  • Required: Toggle if relationship is mandatory

Step 4: Save and Sync

  1. Click Save Field
  2. Click Sync Database to apply changes

Code Approach

Relationships are defined in the second argument to em():

const schema = em(
  {
    // Entity definitions (first argument)
  },
  ({ relation, index }, entities) => {
    // Relationship definitions (second argument)
  }
);

Many-to-One

Child belongs to one parent. Most common relationship type.

import { em, entity, text } from "bknd";

const schema = em(
  {
    users: entity("users", { email: text().required() }),
    posts: entity("posts", { title: text().required() }),
  },
  ({ relation }, { users, posts }) => {
    relation(posts).manyToOne(users);
  }
);

Auto-generated: users_id foreign key column on posts table

Custom field name with mappedBy:

({ relation }, { users, posts }) => {
  relation(posts).manyToOne(users, {
    mappedBy: "author",  // Creates author_id instead of users_id
  });
}

One-to-One

Exclusive 1:1 relationship. Each child belongs to exactly one parent.

const schema = em(
  {
    users: entity("users", { email: text().required() }),
    profiles: entity("profiles", { bio: text() }),
  },
  ({ relation }, { users, profiles }) => {
    relation(profiles).oneToOne(users);
  }
);

Note: One-to-one relationships cannot use $set operator (maintains exclusivity).

Many-to-Many

Both entities can have multiple of the other. Junction table created automatically.

const schema = em(
  {
    posts: entity("posts", { title: text().required() }),
    tags: entity("tags", { name: text().required() }),
  },
  ({ relation }, { posts, tags }) => {
    relation(posts).manyToMany(tags);
  }
);

Auto-generated: posts_tags junction table with posts_id and tags_id columns

Custom junction table name:

({ relation }, { posts, tags }) => {
  relation(posts).manyToMany(tags, {
    connectionTable: "post_tags",  // Custom junction table name
  });
}

Extra fields on junction table:

({ relation }, { users, courses }) => {
  relation(users).manyToMany(courses, {
    connectionTable: "enrollments",
  }, {
    // Extra fields on junction table
    enrolled_at: date(),
    completed: boolean(),
    grade: number(),
  });
}

Self-Referencing

Entity references itself. Common for hierarchies (categories, comments, org charts).

const schema = em(
  {
    categories: entity("categories", { name: text().required() }),
  },
  ({ relation }, { categories }) => {
    relation(categories).manyToOne(categories, {
      mappedBy: "parent",      // FK field: parent_id
      inversedBy: "children",  // Reverse navigation
    });
  }
);

Usage:

  • category.parent_id → Points to parent category
  • Query children: api.data.readMany("categories", { where: { parent_id: 5 } })

Alternative: Direct Foreign Key

Instead of relation(), use .references() on a number field:

const schema = em({
  users: entity("users", { email: text().required() }),
  posts: entity("posts", {
    title: text().required(),
    author_id: number().references("users.id"),
  }),
});

Difference: .references() is simpler but doesn't create inverse navigation or support many-to-many.

Relation Options

ManyToOne / OneToOne Options

OptionTypeDefaultDescription
mappedBystringTarget entity nameFK field name (e.g., author → author_id)
inversedBystringSource entity nameReverse navigation name
requiredbooleanfalseRelationship is mandatory

ManyToMany Options

OptionTypeDefaultDescription
connectionTablestring{source}_{target}Junction table name

Querying Relations

Load Related Data (with)

const api = app.getApi();

// Load posts with their author
const posts = await api.data.readMany("posts", {
  with: {
    users: { select: ["email", "name"] },
  },
});
// Result: [{ id: 1, title: "...", users: { email: "...", name: "..." } }]

Filter by Relation

// Posts by specific author
const posts = await api.data.readMany("posts", {
  where: { author_id: 5 },
});

// Using join for complex filters
const posts = await api.data.readMany("posts", {
  join: {
    users: { where: { email: "[email protected]" } },
  },
});

Many-to-Many Operations

// Attach tags to post
await api.data.updateOne("posts", 1, {
  tags: { $attach: [1, 2, 3] },  // Tag IDs
});

// Detach tags
await api.data.updateOne("posts", 1, {
  tags: { $detach: [2] },
});

// Replace all tags
await api.data.updateOne("posts", 1, {
  tags: { $set: [4, 5] },
});

Many-to-One Operations

// Set author on post
await api.data.updateOne("posts", 1, {
  users: { $set: 5 },  // User ID
});

Common Patterns

Blog with Authors and Tags

const schema = em(
  {
    users: entity("users", {
      email: text().required().unique(),
      name: text(),
    }),
    posts: entity("posts", {
      title: text().required(),
      content: text(),
      published: boolean(),
    }),
    tags: entity("tags", {
      name: text().required().unique(),
    }),
  },
  ({ relation }, { users, posts, tags }) => {
    // Post has one author
    relation(posts).manyToOne(users, { mappedBy: "author" });

    // Posts have many tags
    relation(posts).manyToMany(tags);
  }
);

E-commerce Orders

const schema = em(
  {
    customers: entity("customers", { email: text().required() }),
    orders: entity("orders", { total: number() }),
    products: entity("products", { name: text().required(), price: number() }),
  },
  ({ relation }, { customers, orders, products }) => {
    // Order belongs to customer
    relation(orders).manyToOne(customers);

    // Order has many products (with quantity)
    relation(orders).manyToMany(products, {
      connectionTable: "order_items",
    }, {
      quantity: number().required(),
      unit_price: number().required(),
    });
  }
);

Nested Categories

const schema = em(
  {
    categories: entity("categories", {
      name: text().required(),
      slug: text().required().unique(),
    }),
  },
  ({ relation }, { categories }) => {
    relation(categories).manyToOne(categories, {
      mappedBy: "parent",
      inversedBy: "children",
    });
  }
);

// Usage: Get all children of category 5
const children = await api.data.readMany("categories", {
  where: { parent_id: 5 },
});

Common Pitfalls

Entity Not Found

Error: Entity "user" not found

Fix: Entity names are plural by convention. Use users not user.

// Wrong
relation(posts).manyToOne(user);

// Correct
relation(posts).manyToOne(users);

Circular Reference Error

Error: Circular dependency detected

Fix: For self-referencing, use proper options:

// Correct self-reference
relation(categories).manyToOne(categories, {
  mappedBy: "parent",
  inversedBy: "children",
});

Foreign Key Naming Conflict

Error: Field "users_id" already exists

Fix: Use mappedBy to specify a different field name:

// If you already have users_id, use a different name
relation(posts).manyToOne(users, { mappedBy: "author" });  // Creates author_id

Many-to-Many $set on One-to-One

Error: Cannot use $set on one-to-one relation

Fix: One-to-one maintains exclusivity differently. Use $create instead:

// For one-to-one
await api.data.updateOne("users", 1, {
  profiles: { $create: { bio: "Hello" } },
});

Missing Entity in Destructure

Error: Cannot read property 'manyToOne' of undefined

Fix: Ensure entity is destructured from second callback parameter:

// Wrong - missing users in destructure
({ relation }, { posts }) => {
  relation(posts).manyToOne(users);  // users is undefined
}

// Correct
({ relation }, { users, posts }) => {
  relation(posts).manyToOne(users);
}

Relation Changes Not Applying

Problem: Added relation but not seeing FK column.

Fixes:

  1. Restart server (schema syncs on startup)
  2. Verify relation is in second em() argument
  3. Check for syntax errors

Verification

Check Foreign Key Created

npx bknd debug paths
# Look for the FK field in entity output

Test Relation in Code

const api = app.getApi();

// Create parent
const user = await api.data.createOne("users", { email: "[email protected]" });

// Create child with relation
const post = await api.data.createOne("posts", {
  title: "Test Post",
  author_id: user.data.id,
});

// Load with relation
const loaded = await api.data.readOne("posts", post.data.id, {
  with: { users: true },
});
console.log(loaded.data.users);  // { id: 1, email: "[email protected]" }

DOs and DON'Ts

DO:

  • Use plural entity names (users, posts)
  • Use mappedBy for semantic field names (author instead of users)
  • Define relations in the second em() argument
  • Use .references() for simple FK without navigation

DON'T:

  • Use singular entity names in relations
  • Create manual FK fields when using relation() (it creates them automatically)
  • Use $set on one-to-one relations
  • Forget to destructure entities in the callback

Related Skills

  • bknd-create-entity - Create entities before defining relationships
  • bknd-add-field - Add fields including .references() for simple FKs
  • bknd-crud-read - Query related data with with and join
  • bknd-crud-update - Use $attach, $detach, $set for relation updates
  • bknd-query-filter - Advanced filtering on relations

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View