Skip to content

Relationships Example

This comprehensive example demonstrates how to define and use relationships in Dynamite ORM. Learn how to create one-to-many (HasMany) and many-to-one (BelongsTo) relationships, perform nested includes, filter related data, and build complex data structures.

Table of Contents

Relationship Basics

Dynamite supports two types of relationships:

  • HasMany - One-to-many relationship (parent has multiple children)
  • BelongsTo - Many-to-one relationship (child belongs to parent)

Key Concepts

import { HasMany, BelongsTo, NonAttribute } from "@arcaelas/dynamite";

// Parent model (User has many Orders)
class User extends Table<User> {
  @HasMany(() => Order, "user_id")
  declare orders: NonAttribute<HasMany<Order>>;
}

// Child model (Order belongs to User)
class Order extends Table<Order> {
  declare user_id: string; // Foreign key

  @BelongsTo(() => User, "user_id")
  declare user: NonAttribute<BelongsTo<User>>;
}

Important: - Use NonAttribute<> wrapper for relationship fields (they're not stored in DB) - HasMany<T> resolves to T[] (array of related records) - BelongsTo<T> resolves to T | null (single related record or null) - Foreign key field must exist on the child model

One-to-Many (HasMany)

Define a one-to-many relationship where a parent model has multiple related children.

Basic HasMany Example

import {
  Table,
  PrimaryKey,
  Default,
  HasMany,
  CreationOptional,
  NonAttribute
} from "@arcaelas/dynamite";

// User model (parent)
class User extends Table<User> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  declare name: string;
  declare email: string;

  // One-to-many: User has many Posts
  @HasMany(() => Post, "user_id")
  declare posts: NonAttribute<HasMany<Post>>;
}

// Post model (child)
class Post extends Table<Post> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  declare user_id: string; // Foreign key
  declare title: string;
  declare content: string;
}

Loading HasMany Relationships

// Load users with their posts
const users_with_posts = await User.where({}, {
  include: {
    posts: {}
  }
});

users_with_posts.forEach(user => {
  console.log(`${user.name} has ${user.posts.length} posts`);
  user.posts.forEach(post => {
    console.log(`  - ${post.title}`);
  });
});

// Load specific user with posts
const user = await User.first({ id: "user-123" });
if (user) {
  const user_with_posts = await User.where({ id: user.id }, {
    include: { posts: {} }
  });
  console.log(`Posts: ${user_with_posts[0].posts.length}`);
}

Multiple HasMany Relationships

A model can have multiple one-to-many relationships:

class User extends Table<User> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  declare name: string;

  // User has many Posts
  @HasMany(() => Post, "user_id")
  declare posts: NonAttribute<HasMany<Post>>;

  // User has many Comments
  @HasMany(() => Comment, "user_id")
  declare comments: NonAttribute<HasMany<Comment>>;

  // User has many Orders
  @HasMany(() => Order, "user_id")
  declare orders: NonAttribute<HasMany<Order>>;
}

// Load user with all relationships
const users = await User.where({ id: "user-123" }, {
  include: {
    posts: {},
    comments: {},
    orders: {}
  }
});

const user = users[0];
console.log(`Posts: ${user.posts.length}`);
console.log(`Comments: ${user.comments.length}`);
console.log(`Orders: ${user.orders.length}`);

Many-to-One (BelongsTo)

Define a many-to-one relationship where a child model belongs to a single parent.

Basic BelongsTo Example

// Post model (child)
class Post extends Table<Post> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  declare user_id: string; // Foreign key
  declare title: string;
  declare content: string;

  // Many-to-one: Post belongs to User
  @BelongsTo(() => User, "user_id")
  declare user: NonAttribute<BelongsTo<User>>;
}

// User model (parent)
class User extends Table<User> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  declare name: string;
  declare email: string;
}

Loading BelongsTo Relationships

// Load posts with their author
const posts_with_author = await Post.where({}, {
  include: {
    user: {}
  }
});

posts_with_author.forEach(post => {
  console.log(`${post.title} by ${post.user?.name || 'Unknown'}`);
});

// Load specific post with author
const post = await Post.first({ id: "post-123" });
if (post) {
  const post_with_author = await Post.where({ id: post.id }, {
    include: { user: {} }
  });
  console.log(`Author: ${post_with_author[0].user?.name}`);
}

Multiple BelongsTo Relationships

A child model can belong to multiple parents:

class Order extends Table<Order> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  declare user_id: string;
  declare product_id: string;
  declare quantity: number;

  // Order belongs to User
  @BelongsTo(() => User, "user_id")
  declare user: NonAttribute<BelongsTo<User>>;

  // Order belongs to Product
  @BelongsTo(() => Product, "product_id")
  declare product: NonAttribute<BelongsTo<Product>>;
}

// Load order with both relationships
const orders = await Order.where({ id: "order-123" }, {
  include: {
    user: {},
    product: {}
  }
});

const order = orders[0];
console.log(`Customer: ${order.user?.name}`);
console.log(`Product: ${order.product?.name}`);
console.log(`Quantity: ${order.quantity}`);

Nested Relationships

Load relationships that have their own relationships (nested includes).

Two-Level Nesting

// User has many Posts, Post has many Comments
class User extends Table<User> {
  @PrimaryKey()
  declare id: string;
  declare name: string;

  @HasMany(() => Post, "user_id")
  declare posts: NonAttribute<HasMany<Post>>;
}

class Post extends Table<Post> {
  @PrimaryKey()
  declare id: string;
  declare user_id: string;
  declare title: string;

  @BelongsTo(() => User, "user_id")
  declare user: NonAttribute<BelongsTo<User>>;

  @HasMany(() => Comment, "post_id")
  declare comments: NonAttribute<HasMany<Comment>>;
}

class Comment extends Table<Comment> {
  @PrimaryKey()
  declare id: string;
  declare post_id: string;
  declare content: string;
}

// Load users with posts and comments
const users = await User.where({}, {
  include: {
    posts: {
      include: {
        comments: {}
      }
    }
  }
});

users.forEach(user => {
  console.log(`${user.name}:`);
  user.posts.forEach(post => {
    console.log(`  ${post.title} (${post.comments.length} comments)`);
    post.comments.forEach(comment => {
      console.log(`    - ${comment.content}`);
    });
  });
});

Multi-Level Nesting

// Order -> OrderItem -> Product
class Order extends Table<Order> {
  @PrimaryKey()
  declare id: string;
  declare user_id: string;

  @BelongsTo(() => User, "user_id")
  declare user: NonAttribute<BelongsTo<User>>;

  @HasMany(() => OrderItem, "order_id")
  declare items: NonAttribute<HasMany<OrderItem>>;
}

class OrderItem extends Table<OrderItem> {
  @PrimaryKey()
  declare id: string;
  declare order_id: string;
  declare product_id: string;
  declare quantity: number;

  @BelongsTo(() => Product, "product_id")
  declare product: NonAttribute<BelongsTo<Product>>;
}

class Product extends Table<Product> {
  @PrimaryKey()
  declare id: string;
  declare name: string;
  declare price: number;
}

// Load orders with items and products
const orders = await Order.where({}, {
  include: {
    user: {},
    items: {
      include: {
        product: {}
      }
    }
  }
});

orders.forEach(order => {
  console.log(`Order ${order.id} by ${order.user?.name}`);
  order.items.forEach(item => {
    console.log(`  ${item.quantity}x ${item.product?.name} @ $${item.product?.price}`);
  });
});

Filtered Relationships

Apply filters, limits, and ordering to related data.

// Load user with only published posts
const users = await User.where({ id: "user-123" }, {
  include: {
    posts: {
      where: { status: "published" }
    }
  }
});

console.log(`Published posts: ${users[0].posts.length}`);
// Load user with 5 most recent posts
const users = await User.where({ id: "user-123" }, {
  include: {
    posts: {
      limit: 5,
      order: "DESC"
    }
  }
});

console.log(`Recent posts: ${users[0].posts.length}`);

Select Specific Attributes

// Load posts with only user name and email
const posts = await Post.where({}, {
  include: {
    user: {
      attributes: ["id", "name", "email"]
    }
  }
});

posts.forEach(post => {
  console.log(`${post.title} by ${post.user?.name} (${post.user?.email})`);
});

Combined Filters

// Complex relationship query
const users = await User.where({ role: "premium" }, {
  include: {
    orders: {
      where: { status: "completed" },
      limit: 10,
      order: "DESC",
      attributes: ["id", "total", "created_at"]
    },
    posts: {
      where: { published: true },
      limit: 5
    }
  }
});

users.forEach(user => {
  console.log(`${user.name}:`);
  console.log(`  Recent orders: ${user.orders.length}`);
  console.log(`  Published posts: ${user.posts.length}`);
});

Complete E-Commerce Example

Here's a complete e-commerce system demonstrating all relationship patterns:

import {
  Table,
  PrimaryKey,
  Default,
  HasMany,
  BelongsTo,
  CreatedAt,
  UpdatedAt,
  CreationOptional,
  NonAttribute,
  Dynamite
} from "@arcaelas/dynamite";

// User model
class User extends Table<User> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  declare name: string;
  declare email: string;

  @Default(() => "customer")
  declare role: CreationOptional<string>;

  @CreatedAt()
  declare created_at: CreationOptional<string>;

  @UpdatedAt()
  declare updated_at: CreationOptional<string>;

  // Relationships
  @HasMany(() => Order, "user_id")
  declare orders: NonAttribute<HasMany<Order>>;

  @HasMany(() => Review, "user_id")
  declare reviews: NonAttribute<HasMany<Review>>;
}

// Product model
class Product extends Table<Product> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  declare name: string;
  declare description: string;
  declare price: number;

  @Default(() => 0)
  declare stock: CreationOptional<number>;

  @CreatedAt()
  declare created_at: CreationOptional<string>;

  // Relationships
  @HasMany(() => OrderItem, "product_id")
  declare order_items: NonAttribute<HasMany<OrderItem>>;

  @HasMany(() => Review, "product_id")
  declare reviews: NonAttribute<HasMany<Review>>;
}

// Order model
class Order extends Table<Order> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  declare user_id: string;

  @Default(() => "pending")
  declare status: CreationOptional<string>;

  declare total: number;

  @CreatedAt()
  declare created_at: CreationOptional<string>;

  @UpdatedAt()
  declare updated_at: CreationOptional<string>;

  // Relationships
  @BelongsTo(() => User, "user_id")
  declare user: NonAttribute<BelongsTo<User>>;

  @HasMany(() => OrderItem, "order_id")
  declare items: NonAttribute<HasMany<OrderItem>>;
}

// OrderItem model
class OrderItem extends Table<OrderItem> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  declare order_id: string;
  declare product_id: string;
  declare quantity: number;
  declare price: number;

  // Relationships
  @BelongsTo(() => Order, "order_id")
  declare order: NonAttribute<BelongsTo<Order>>;

  @BelongsTo(() => Product, "product_id")
  declare product: NonAttribute<BelongsTo<Product>>;
}

// Review model
class Review extends Table<Review> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  declare user_id: string;
  declare product_id: string;
  declare rating: number;
  declare comment: string;

  @CreatedAt()
  declare created_at: CreationOptional<string>;

  // Relationships
  @BelongsTo(() => User, "user_id")
  declare user: NonAttribute<BelongsTo<User>>;

  @BelongsTo(() => Product, "product_id")
  declare product: NonAttribute<BelongsTo<Product>>;
}

// Configure DynamoDB and register all tables
const dynamite = new Dynamite({
  region: "us-east-1",
  endpoint: "http://localhost:8000",
  tables: [User, Product, Order, OrderItem, Review],
  credentials: {
    accessKeyId: "test",
    secretAccessKey: "test"
  }
});

// Main application
async function main() {
  // Connect and sync tables
  dynamite.connect();
  await dynamite.sync();
  console.log("=== E-Commerce Relationships Example ===\n");

  // 1. Create users
  console.log("1. Creating users...");
  const user1 = await User.create({
    name: "John Doe",
    email: "john@example.com"
  });
  const user2 = await User.create({
    name: "Jane Smith",
    email: "jane@example.com"
  });
  console.log(`Created: ${user1.name}, ${user2.name}\n`);

  // 2. Create products
  console.log("2. Creating products...");
  const product1 = await Product.create({
    name: "Laptop",
    description: "High-performance laptop",
    price: 999.99,
    stock: 10
  });
  const product2 = await Product.create({
    name: "Mouse",
    description: "Wireless mouse",
    price: 29.99,
    stock: 50
  });
  const product3 = await Product.create({
    name: "Keyboard",
    description: "Mechanical keyboard",
    price: 79.99,
    stock: 30
  });
  console.log(`Created: ${product1.name}, ${product2.name}, ${product3.name}\n`);

  // 3. Create orders
  console.log("3. Creating orders...");
  const order1 = await Order.create({
    user_id: user1.id,
    total: 1109.97,
    status: "pending"
  });
  const order2 = await Order.create({
    user_id: user2.id,
    total: 79.99,
    status: "completed"
  });
  console.log(`Created: Order ${order1.id}, Order ${order2.id}\n`);

  // 4. Create order items
  console.log("4. Creating order items...");
  await OrderItem.create({
    order_id: order1.id,
    product_id: product1.id,
    quantity: 1,
    price: 999.99
  });
  await OrderItem.create({
    order_id: order1.id,
    product_id: product2.id,
    quantity: 2,
    price: 29.99
  });
  await OrderItem.create({
    order_id: order1.id,
    product_id: product3.id,
    quantity: 1,
    price: 79.99
  });
  await OrderItem.create({
    order_id: order2.id,
    product_id: product3.id,
    quantity: 1,
    price: 79.99
  });
  console.log("Order items created\n");

  // 5. Create reviews
  console.log("5. Creating reviews...");
  await Review.create({
    user_id: user1.id,
    product_id: product1.id,
    rating: 5,
    comment: "Excellent laptop! Very fast and reliable."
  });
  await Review.create({
    user_id: user2.id,
    product_id: product3.id,
    rating: 4,
    comment: "Great keyboard, but a bit loud."
  });
  console.log("Reviews created\n");

  // 6. Load user with orders
  console.log("6. Loading user with orders...");
  const users_with_orders = await User.where({ id: user1.id }, {
    include: {
      orders: {}
    }
  });
  const user_with_orders = users_with_orders[0];
  console.log(`${user_with_orders.name} has ${user_with_orders.orders.length} order(s)`);
  user_with_orders.orders.forEach(order => {
    console.log(`  Order ${order.id}: $${order.total} (${order.status})`);
  });
  console.log();

  // 7. Load order with items and products
  console.log("7. Loading order with items and products...");
  const orders_with_items = await Order.where({ id: order1.id }, {
    include: {
      user: {},
      items: {
        include: {
          product: {}
        }
      }
    }
  });
  const order_with_items = orders_with_items[0];
  console.log(`Order ${order_with_items.id} by ${order_with_items.user?.name}`);
  console.log(`Total: $${order_with_items.total}`);
  console.log("Items:");
  order_with_items.items.forEach(item => {
    console.log(`  ${item.quantity}x ${item.product?.name} @ $${item.price}`);
  });
  console.log();

  // 8. Load product with reviews and reviewers
  console.log("8. Loading product with reviews...");
  const products_with_reviews = await Product.where({ id: product1.id }, {
    include: {
      reviews: {
        include: {
          user: {}
        }
      }
    }
  });
  const product_with_reviews = products_with_reviews[0];
  console.log(`${product_with_reviews.name} - Reviews:`);
  product_with_reviews.reviews.forEach(review => {
    console.log(`  ${review.rating}/5 by ${review.user?.name}`);
    console.log(`  "${review.comment}"`);
  });
  console.log();

  // 9. Load user with orders, reviews, and related data
  console.log("9. Loading user with all relationships...");
  const users_complete = await User.where({ id: user1.id }, {
    include: {
      orders: {
        include: {
          items: {
            include: {
              product: {}
            }
          }
        }
      },
      reviews: {
        include: {
          product: {}
        }
      }
    }
  });
  const user_complete = users_complete[0];
  console.log(`${user_complete.name}:`);
  console.log(`  Orders: ${user_complete.orders.length}`);
  user_complete.orders.forEach(order => {
    console.log(`    - Order ${order.id}: ${order.items.length} items, $${order.total}`);
  });
  console.log(`  Reviews: ${user_complete.reviews.length}`);
  user_complete.reviews.forEach(review => {
    console.log(`    - ${review.rating}/5 for ${review.product?.name}`);
  });
  console.log();

  // 10. Load orders with filters
  console.log("10. Loading completed orders only...");
  const all_users_with_completed = await User.where({}, {
    include: {
      orders: {
        where: { status: "completed" }
      }
    }
  });
  all_users_with_completed.forEach(user => {
    if (user.orders.length > 0) {
      console.log(`${user.name}: ${user.orders.length} completed order(s)`);
    }
  });
  console.log();

  console.log("=== All relationship operations completed ===");
}

// Run the application
main().catch(console.error);

Expected Output

=== E-Commerce Relationships Example ===

1. Creating users...
Created: John Doe, Jane Smith

2. Creating products...
Created: Laptop, Mouse, Keyboard

3. Creating orders...
Created: Order 550e8400-..., Order 6ba7b810-...

4. Creating order items...
Order items created

5. Creating reviews...
Reviews created

6. Loading user with orders...
John Doe has 1 order(s)
  Order 550e8400-...: $1109.97 (pending)

7. Loading order with items and products...
Order 550e8400-... by John Doe
Total: $1109.97
Items:
  1x Laptop @ $999.99
  2x Mouse @ $29.99
  1x Keyboard @ $79.99

8. Loading product with reviews...
Laptop - Reviews:
  5/5 by John Doe
  "Excellent laptop! Very fast and reliable."

9. Loading user with all relationships...
John Doe:
  Orders: 1
    - Order 550e8400-...: 3 items, $1109.97
  Reviews: 1
    - 5/5 for Laptop

10. Loading completed orders only...
Jane Smith: 1 completed order(s)

=== All relationship operations completed ===

Advanced Patterns

Self-Referential Relationships

Models can have relationships with themselves:

class Category extends Table<Category> {
  @PrimaryKey()
  declare id: string;

  declare name: string;
  declare parent_id: string | null;

  // Category has many child categories
  @HasMany(() => Category, "parent_id")
  declare children: NonAttribute<HasMany<Category>>;

  // Category belongs to parent category
  @BelongsTo(() => Category, "parent_id")
  declare parent: NonAttribute<BelongsTo<Category>>;
}

// Load category tree
const categories = await Category.where({ parent_id: null }, {
  include: {
    children: {
      include: {
        children: {}
      }
    }
  }
});

Many-to-Many Relationships (via Junction Table)

Implement many-to-many using a junction table:

// Student model
class Student extends Table<Student> {
  @PrimaryKey()
  declare id: string;
  declare name: string;

  @HasMany(() => Enrollment, "student_id")
  declare enrollments: NonAttribute<HasMany<Enrollment>>;
}

// Course model
class Course extends Table<Course> {
  @PrimaryKey()
  declare id: string;
  declare name: string;

  @HasMany(() => Enrollment, "course_id")
  declare enrollments: NonAttribute<HasMany<Enrollment>>;
}

// Junction table
class Enrollment extends Table<Enrollment> {
  @PrimaryKey()
  declare id: string;

  declare student_id: string;
  declare course_id: string;
  declare grade: string;

  @BelongsTo(() => Student, "student_id")
  declare student: NonAttribute<BelongsTo<Student>>;

  @BelongsTo(() => Course, "course_id")
  declare course: NonAttribute<BelongsTo<Course>>;
}

// Load student with courses
const students = await Student.where({ id: "student-123" }, {
  include: {
    enrollments: {
      include: {
        course: {}
      }
    }
  }
});

const student = students[0];
console.log(`${student.name}'s courses:`);
student.enrollments.forEach(enrollment => {
  console.log(`  ${enrollment.course?.name} - Grade: ${enrollment.grade}`);
});

Polymorphic Relationships

Implement polymorphic relationships using type fields:

class Comment extends Table<Comment> {
  @PrimaryKey()
  declare id: string;

  declare commentable_type: string; // "Post" or "Video"
  declare commentable_id: string;
  declare content: string;

  // Load polymorphic relationship manually
  async get_commentable() {
    if (this.commentable_type === "Post") {
      return await Post.first({ id: this.commentable_id });
    } else if (this.commentable_type === "Video") {
      return await Video.first({ id: this.commentable_id });
    }
    return null;
  }
}

Best Practices

1. Use NonAttribute for Relationships

// Good - marked as NonAttribute
@HasMany(() => Order, "user_id")
declare orders: NonAttribute<HasMany<Order>>;

// Bad - will try to save to database
@HasMany(() => Order, "user_id")
declare orders: Order[];

2. Define Foreign Keys Explicitly

// Good - explicit foreign key
class Order extends Table<Order> {
  declare user_id: string; // Foreign key field

  @BelongsTo(() => User, "user_id")
  declare user: NonAttribute<BelongsTo<User>>;
}

// Bad - missing foreign key field
class Order extends Table<Order> {
  @BelongsTo(() => User, "user_id")
  declare user: NonAttribute<BelongsTo<User>>;
}

3. Use Arrow Functions in Decorators

// Good - arrow function (avoids circular dependency)
@HasMany(() => Order, "user_id")
declare orders: NonAttribute<HasMany<Order>>;

// Bad - direct reference (can cause circular dependency issues)
@HasMany(Order, "user_id")
declare orders: NonAttribute<HasMany<Order>>;

4. Filter Relationships for Performance

// Good - load only what you need
const users = await User.where({}, {
  include: {
    orders: {
      where: { status: "completed" },
      limit: 10,
      attributes: ["id", "total"]
    }
  }
});

// Bad - loading all orders with all fields
const users = await User.where({}, {
  include: {
    orders: {}
  }
});

5. Avoid N+1 Queries

// Good - load relationships in one query
const posts = await Post.where({}, {
  include: {
    user: {},
    comments: {}
  }
});

// Bad - N+1 queries
const posts = await Post.where({});
for (const post of posts) {
  const user = await User.first({ id: post.user_id });
  const comments = await Comment.where({ post_id: post.id });
}

Next Steps

API References

Additional Topics

  • Eager Loading - Load all related data upfront
  • Lazy Loading - Load relationships on-demand
  • Relationship Caching - Cache frequently accessed relationships
  • Circular Dependencies - Handle circular model references

Happy coding with Dynamite relationships!