Overview
Esix provides a simple way to define relationships between models using the hasMany() method. This allows you to express one-to-many relationships and easily query related records.
The hasMany() Method
The hasMany() method defines a one-to-many relationship where the parent model has multiple related child models. It returns a QueryBuilder that you can chain with other query methods.
Basic Usage
class User extends BaseModel {
public name = ''
public email = ''
}
class Post extends BaseModel {
public title = ''
public content = ''
public userId = '' // Foreign key
}
// Get all posts for a user
const user = await User.find('user-id-123')
const posts = await user.hasMany(Post).get()
How It Works
By default, hasMany() automatically determines the foreign key name by converting the parent model’s class name to camelCase and appending “Id”.
For a User model, the default foreign key is userId. For a BlogPost model, it would be blogPostId.
Method Signature
hasMany<T extends BaseModel>(
ctor: ObjectType<T>,
foreignKey?: string,
localKey?: string
): QueryBuilder<T>
Parameters:
ctor - The related model’s constructor
foreignKey - (Optional) The foreign key field name on the related model (defaults to {modelName}Id)
localKey - (Optional) The local key field name on the parent model (defaults to id)
Custom Foreign Keys
You can specify a custom foreign key if your relationship doesn’t follow the default naming convention:
class Author extends BaseModel {
public name = ''
}
class Book extends BaseModel {
public title = ''
public authorId = '' // Custom foreign key
}
const author = await Author.find('author-123')
const books = await author.hasMany(Book, 'authorId').get()
Custom Local Keys
You can also specify a custom local key if you want to join on a field other than id:
class Department extends BaseModel {
public code = ''
public name = ''
}
class Employee extends BaseModel {
public name = ''
public departmentCode = ''
}
const department = await Department.findBy('code', 'ENG')
const employees = await department.hasMany(Employee, 'departmentCode', 'code').get()
Chaining Query Methods
Since hasMany() returns a QueryBuilder, you can chain additional query methods to filter, sort, or limit the results:
const user = await User.find('user-123')
// Get only published posts
const publishedPosts = await user
.hasMany(Post)
.where('status', 'published')
.get()
// Get the latest 5 posts
const latestPosts = await user
.hasMany(Post)
.orderBy('createdAt', 'desc')
.limit(5)
.get()
// Count total posts
const postCount = await user.hasMany(Post).count()
// Get post titles only
const titles = await user.hasMany(Post).pluck('title')
Real-World Examples
Blog with Users and Posts
class User extends BaseModel {
public username = ''
public email = ''
}
class Post extends BaseModel {
public title = ''
public content = ''
public userId = ''
public status = 'draft' // 'draft' | 'published'
}
class Comment extends BaseModel {
public content = ''
public postId = ''
public userId = ''
}
// Get all posts by a user
const user = await User.findBy('username', 'john')
const posts = await user.hasMany(Post).get()
// Get published posts only
const publishedPosts = await user
.hasMany(Post)
.where('status', 'published')
.orderBy('createdAt', 'desc')
.get()
// Get comments for a post
const post = await Post.find('post-123')
const comments = await post.hasMany(Comment).get()
E-commerce with Orders and Items
class Customer extends BaseModel {
public name = ''
public email = ''
}
class Order extends BaseModel {
public customerId = ''
public status = 'pending'
public total = 0
}
class OrderItem extends BaseModel {
public orderId = ''
public productName = ''
public quantity = 0
public price = 0
}
// Get all orders for a customer
const customer = await Customer.findBy('email', 'customer@example.com')
const orders = await customer.hasMany(Order).get()
// Get completed orders only
const completedOrders = await customer
.hasMany(Order)
.where('status', 'completed')
.orderBy('createdAt', 'desc')
.get()
// Calculate total spending
const totalSpent = await customer.hasMany(Order).sum('total')
// Get items for an order
const order = await Order.find('order-456')
const items = await order.hasMany(OrderItem).get()
Type Safety
The hasMany() method is fully type-safe. TypeScript will ensure that:
- The related model class is valid
- The foreign and local keys exist on the respective models
- All chained query methods are properly typed
const user = await User.find('user-123')
// TypeScript knows this returns Post[]
const posts: Post[] = await user.hasMany(Post).get()
// Type error: 'invalidField' doesn't exist on Post
const invalid = await user.hasMany(Post).where('invalidField', 'value').get()
Implementation Details
The hasMany() method is implemented in the BaseModel class (base-model.ts:429-440). It:
- Creates a new QueryBuilder for the related model
- Automatically determines or uses the provided foreign key
- Automatically determines or uses the provided local key
- Adds a
where clause matching the foreign key to the local key value
- Returns the QueryBuilder for further chaining
Behind the scenes, hasMany() uses the QueryBuilder’s where() method to filter related records. All sanitization and security measures apply automatically.
Next Steps