Ejemplo de Relaciones¶
Este ejemplo completo demuestra cómo definir y usar relaciones en Dynamite ORM. Aprende cómo crear relaciones uno-a-muchos (HasMany) y muchos-a-uno (BelongsTo), realizar includes anidados, filtrar datos relacionados y construir estructuras de datos complejas.
Tabla de Contenidos¶
- Conceptos Basicos de Relaciones
- Uno-a-Muchos (HasMany)
- Muchos-a-Uno (BelongsTo)
- Relaciones Anidadas
- Relaciones Filtradas
- Ejemplo Completo de E-Commerce
- Salida Esperada
- Patrones Avanzados
- Mejores Practicas
Conceptos Básicos de Relaciones¶
Dynamite soporta dos tipos de relaciones:
- HasMany - Relación uno-a-muchos (padre tiene múltiples hijos)
- BelongsTo - Relación muchos-a-uno (hijo pertenece a padre)
Conceptos Clave¶
import { HasMany, BelongsTo, NonAttribute } from "@arcaelas/dynamite";
// Modelo padre (User tiene muchos Orders)
class User extends Table<User> {
@HasMany(() => Order, "user_id")
declare orders: NonAttribute<HasMany<Order>>;
}
// Modelo hijo (Order pertenece a User)
class Order extends Table<Order> {
declare user_id: string; // Clave foránea
@BelongsTo(() => User, "user_id")
declare user: NonAttribute<BelongsTo<User>>;
}
Importante: - Usa el wrapper NonAttribute<> para campos de relación (no se almacenan en DB) - HasMany<T> se resuelve a T[] (array de registros relacionados) - BelongsTo<T> se resuelve a T | null (un único registro relacionado o null) - El campo de clave foránea debe existir en el modelo hijo
Uno-a-Muchos (HasMany)¶
Define una relación uno-a-muchos donde un modelo padre tiene múltiples hijos relacionados.
Ejemplo Básico de HasMany¶
import {
Table,
PrimaryKey,
Default,
HasMany,
CreationOptional,
NonAttribute
} from "@arcaelas/dynamite";
// Modelo User (padre)
class User extends Table<User> {
@PrimaryKey()
@Default(() => crypto.randomUUID())
declare id: CreationOptional<string>;
declare name: string;
declare email: string;
// Uno-a-muchos: User tiene muchos Posts
@HasMany(() => Post, "user_id")
declare posts: NonAttribute<HasMany<Post>>;
}
// Modelo Post (hijo)
class Post extends Table<Post> {
@PrimaryKey()
@Default(() => crypto.randomUUID())
declare id: CreationOptional<string>;
declare user_id: string; // Clave foránea
declare title: string;
declare content: string;
}
Cargar Relaciones HasMany¶
// Cargar usuarios con sus 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}`);
});
});
// Cargar usuario específico con 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}`);
}
Múltiples Relaciones HasMany¶
Un modelo puede tener múltiples relaciones uno-a-muchos:
class User extends Table<User> {
@PrimaryKey()
@Default(() => crypto.randomUUID())
declare id: CreationOptional<string>;
declare name: string;
// User tiene muchos Posts
@HasMany(() => Post, "user_id")
declare posts: NonAttribute<HasMany<Post>>;
// User tiene muchos Comments
@HasMany(() => Comment, "user_id")
declare comments: NonAttribute<HasMany<Comment>>;
// User tiene muchos Orders
@HasMany(() => Order, "user_id")
declare orders: NonAttribute<HasMany<Order>>;
}
// Cargar usuario con todas las relaciones
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}`);
Muchos-a-Uno (BelongsTo)¶
Define una relación muchos-a-uno donde un modelo hijo pertenece a un solo padre.
Ejemplo Básico de BelongsTo¶
// Modelo Post (hijo)
class Post extends Table<Post> {
@PrimaryKey()
@Default(() => crypto.randomUUID())
declare id: CreationOptional<string>;
declare user_id: string; // Clave foránea
declare title: string;
declare content: string;
// Muchos-a-uno: Post pertenece a User
@BelongsTo(() => User, "user_id")
declare user: NonAttribute<BelongsTo<User>>;
}
// Modelo User (padre)
class User extends Table<User> {
@PrimaryKey()
@Default(() => crypto.randomUUID())
declare id: CreationOptional<string>;
declare name: string;
declare email: string;
}
Cargar Relaciones BelongsTo¶
// Cargar posts con su autor
const posts_with_author = await Post.where({}, {
include: {
user: {}
}
});
posts_with_author.forEach(post => {
console.log(`${post.title} by ${post.user?.name || 'Unknown'}`);
});
// Cargar post específico con autor
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}`);
}
Múltiples Relaciones BelongsTo¶
Un modelo hijo puede pertenecer a múltiples padres:
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 pertenece a User
@BelongsTo(() => User, "user_id")
declare user: NonAttribute<BelongsTo<User>>;
// Order pertenece a Product
@BelongsTo(() => Product, "product_id")
declare product: NonAttribute<BelongsTo<Product>>;
}
// Cargar orden con ambas relaciones
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}`);
Relaciones Anidadas¶
Cargar relaciones que tienen sus propias relaciones (includes anidados).
Anidamiento de Dos Niveles¶
// User tiene muchos Posts, Post tiene muchos 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;
}
// Cargar usuarios con posts y comentarios
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}`);
});
});
});
Anidamiento Multi-Nivel¶
// 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;
}
// Cargar órdenes con items y productos
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}`);
});
});
Relaciones Filtradas¶
Aplicar filtros, límites y ordenamiento a datos relacionados.
Filtrar Registros Relacionados¶
// Cargar usuario solo con posts publicados
const users = await User.where({ id: "user-123" }, {
include: {
posts: {
where: { status: "published" }
}
}
});
console.log(`Published posts: ${users[0].posts.length}`);
Limitar Registros Relacionados¶
// Cargar usuario con 5 posts más recientes
const users = await User.where({ id: "user-123" }, {
include: {
posts: {
limit: 5,
order: "DESC"
}
}
});
console.log(`Recent posts: ${users[0].posts.length}`);
Seleccionar Atributos Específicos¶
// Cargar posts solo con nombre y email del usuario
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})`);
});
Filtros Combinados¶
// Consulta de relación compleja
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}`);
});
Ejemplo Completo de E-Commerce¶
Aquí hay un sistema completo de e-commerce que demuestra todos los patrones de relaciones:
import {
Table,
PrimaryKey,
Default,
HasMany,
BelongsTo,
CreatedAt,
UpdatedAt,
CreationOptional,
NonAttribute,
Dynamite
} from "@arcaelas/dynamite";
// Modelo User
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>;
// Relaciones
@HasMany(() => Order, "user_id")
declare orders: NonAttribute<HasMany<Order>>;
@HasMany(() => Review, "user_id")
declare reviews: NonAttribute<HasMany<Review>>;
}
// Modelo Product
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>;
// Relaciones
@HasMany(() => OrderItem, "product_id")
declare order_items: NonAttribute<HasMany<OrderItem>>;
@HasMany(() => Review, "product_id")
declare reviews: NonAttribute<HasMany<Review>>;
}
// Modelo Order
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>;
// Relaciones
@BelongsTo(() => User, "user_id")
declare user: NonAttribute<BelongsTo<User>>;
@HasMany(() => OrderItem, "order_id")
declare items: NonAttribute<HasMany<OrderItem>>;
}
// Modelo OrderItem
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;
// Relaciones
@BelongsTo(() => Order, "order_id")
declare order: NonAttribute<BelongsTo<Order>>;
@BelongsTo(() => Product, "product_id")
declare product: NonAttribute<BelongsTo<Product>>;
}
// Modelo Review
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>;
// Relaciones
@BelongsTo(() => User, "user_id")
declare user: NonAttribute<BelongsTo<User>>;
@BelongsTo(() => Product, "product_id")
declare product: NonAttribute<BelongsTo<Product>>;
}
// Configurar DynamoDB y registrar todas las tablas
const dynamite = new Dynamite({
region: "us-east-1",
endpoint: "http://localhost:8000",
tables: [User, Product, Order, OrderItem, Review],
credentials: {
accessKeyId: "test",
secretAccessKey: "test"
}
});
// Aplicación principal
async function main() {
// Conectar y sincronizar tablas
dynamite.connect();
await dynamite.sync();
console.log("=== E-Commerce Relationships Example ===\n");
// 1. Crear usuarios
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. Crear productos
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. Crear órdenes
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. Crear items de orden
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. Crear reseñas
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. Cargar usuario con órdenes
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. Cargar orden con items y productos
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. Cargar producto con reseñas y revisores
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. Cargar usuario con todas las relaciones
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. Cargar órdenes con filtros
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 ===");
}
// Ejecutar la aplicación
main().catch(console.error);
Salida Esperada¶
=== 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 ===
Patrones Avanzados¶
Relaciones Auto-Referenciales¶
Los modelos pueden tener relaciones consigo mismos:
class Category extends Table<Category> {
@PrimaryKey()
declare id: string;
declare name: string;
declare parent_id: string | null;
// Category tiene muchas categorías hijas
@HasMany(() => Category, "parent_id")
declare children: NonAttribute<HasMany<Category>>;
// Category pertenece a categoría padre
@BelongsTo(() => Category, "parent_id")
declare parent: NonAttribute<BelongsTo<Category>>;
}
// Cargar árbol de categorías
const categories = await Category.where({ parent_id: null }, {
include: {
children: {
include: {
children: {}
}
}
}
});
Relaciones Muchos-a-Muchos (vía Tabla de Unión)¶
Implementar muchos-a-muchos usando tabla de unión:
// Modelo Student
class Student extends Table<Student> {
@PrimaryKey()
declare id: string;
declare name: string;
@HasMany(() => Enrollment, "student_id")
declare enrollments: NonAttribute<HasMany<Enrollment>>;
}
// Modelo Course
class Course extends Table<Course> {
@PrimaryKey()
declare id: string;
declare name: string;
@HasMany(() => Enrollment, "course_id")
declare enrollments: NonAttribute<HasMany<Enrollment>>;
}
// Tabla de unión
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>>;
}
// Cargar estudiante con cursos
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}`);
});
Relaciones Polimórficas¶
Implementar relaciones polimórficas usando campos de tipo:
class Comment extends Table<Comment> {
@PrimaryKey()
declare id: string;
declare commentable_type: string; // "Post" o "Video"
declare commentable_id: string;
declare content: string;
// Cargar relación polimórfica manualmente
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;
}
}
Mejores Prácticas¶
1. Usar NonAttribute para Relaciones¶
// Bueno - marcado como NonAttribute
@HasMany(() => Order, "user_id")
declare orders: NonAttribute<HasMany<Order>>;
// Malo - intentará guardar en la base de datos
@HasMany(() => Order, "user_id")
declare orders: Order[];
2. Definir Claves Foráneas Explícitamente¶
// Bueno - clave foránea explícita
class Order extends Table<Order> {
declare user_id: string; // Campo de clave foránea
@BelongsTo(() => User, "user_id")
declare user: NonAttribute<BelongsTo<User>>;
}
// Malo - falta campo de clave foránea
class Order extends Table<Order> {
@BelongsTo(() => User, "user_id")
declare user: NonAttribute<BelongsTo<User>>;
}
3. Usar Funciones Flecha en Decoradores¶
// Bueno - función flecha (evita dependencia circular)
@HasMany(() => Order, "user_id")
declare orders: NonAttribute<HasMany<Order>>;
// Malo - referencia directa (puede causar problemas de dependencia circular)
@HasMany(Order, "user_id")
declare orders: NonAttribute<HasMany<Order>>;
4. Filtrar Relaciones para Rendimiento¶
// Bueno - cargar solo lo que necesitas
const users = await User.where({}, {
include: {
orders: {
where: { status: "completed" },
limit: 10,
attributes: ["id", "total"]
}
}
});
// Malo - cargar todas las órdenes con todos los campos
const users = await User.where({}, {
include: {
orders: {}
}
});
5. Evitar Consultas N+1¶
// Bueno - cargar relaciones en una consulta
const posts = await Post.where({}, {
include: {
user: {},
comments: {}
}
});
// Malo - consultas N+1
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 });
}
Próximos Pasos¶
Documentación Relacionada¶
- Ejemplo de Modelo Básico - Operaciones CRUD simples
- Ejemplo de Consultas Avanzadas - Consultas complejas y paginación
Referencias de API¶
- Decorador HasMany - Documentación completa de HasMany
- Decorador BelongsTo - Documentación completa de BelongsTo
- Consultas Avanzadas - Consultas complejas con relaciones
¡Feliz codificación con relaciones de Dynamite!