adonis-odm
v0.3.1
Published
A comprehensive MongoDB ODM for AdonisJS with Lucid-style API, type-safe relationships, embedded documents, and transaction support
Maintainers
Readme
MongoDB ODM for AdonisJS v6
A comprehensive MongoDB Object Document Mapper (ODM) for AdonisJS v6 that provides a familiar Lucid ORM-like interface for working with MongoDB databases. Built with TypeScript for maximum type safety and developer experience.
✨ Features
Core Features
- 🎯 Familiar API: 100% Lucid ORM-compatible interface for easy adoption
- 🏗️ Decorator-based Models: Use decorators to define your model schema and relationships
- 🔍 Fluent Query Builder: Chainable query methods with MongoDB-specific operations
- 📅 Automatic Timestamps: Auto-managed
createdAtandupdatedAtfields - 🔄 Model Lifecycle: Track model state with
$isPersisted,$dirty, etc. - 📄 Pagination: Built-in pagination support with metadata
- 🔗 Connection Management: Multiple MongoDB connection support
- 🛡️ Type Safety: Full TypeScript support with IntelliSense and compile-time checking
Advanced Features
- 💾 Database Transactions: Full ACID transaction support with managed and manual modes
- 📦 Embedded Documents: Type-safe embedded document support with full CRUD operations
- 🔗 Relationships: Type-safe referenced relationships (@hasOne, @hasMany, @belongsTo)
- 🪝 Lifecycle Hooks: Comprehensive hook system (beforeSave, afterSave, beforeCreate, etc.)
- 🔍 Advanced Querying: Complex filtering, aggregation, and embedded document querying
- 🌱 Database Seeders: Comprehensive seeding system with environment control, execution ordering, and dependency management
- ⚡ Performance: Bulk operations, connection pooling, and optimized queries
- 🛠️ CLI Tools: Ace commands for model generation, seeders, and database operations
- 🧪 Testing Support: Built-in testing utilities and Docker integration
Installation
Install the package from the npm registry as follows:
npm i adonis-odmyarn add adonis-odmpnpm add adonis-odmNext, configure the package by running the following ace command:
node ace configure adonis-odmThe configure command will:
- Register the MongoDB provider inside the
adonisrc.tsfile - Create the
config/odm.tsconfiguration file - Add environment variables to your
.envfile (preserving existing values) - Set up validation rules for environment variables
🔒 Environment Variable Preservation: The configure command intelligently preserves any existing MongoDB environment variables in your
.envfile. Only new variables that don't already exist will be added, ensuring your custom configuration values are never overwritten.
Configuration
The configuration for the ODM is stored inside the config/odm.ts file. You can define one or more NoSQL database connections inside this file. Currently supports MongoDB, with DynamoDB support planned.
import env from '#start/env'
import { defineConfig } from 'adonis-odm'
const odmConfig = defineConfig({
connection: 'mongodb',
connections: {
mongodb: {
client: 'mongodb',
connection: {
// Option 1: Use a full URI
url: env.get('MONGO_URI'),
// Option 2: Use individual components (if url is not provided)
host: env.get('MONGO_HOST', 'localhost'),
port: env.get('MONGO_PORT', 27017),
database: env.get('MONGO_DATABASE'),
// MongoDB connection options
options: {
maxPoolSize: env.get('MONGO_MAX_POOL_SIZE', 10),
minPoolSize: env.get('MONGO_MIN_POOL_SIZE', 0),
maxIdleTimeMS: env.get('MONGO_MAX_IDLE_TIME_MS', 30000),
serverSelectionTimeoutMS: env.get('MONGO_SERVER_SELECTION_TIMEOUT_MS', 5000),
socketTimeoutMS: env.get('MONGO_SOCKET_TIMEOUT_MS', 0),
connectTimeoutMS: env.get('MONGO_CONNECT_TIMEOUT_MS', 10000),
},
},
},
},
// Auto-connect to MongoDB when the application starts
// Set to false in test environments to prevent connection attempts
autoConnect: env.get('NODE_ENV') !== 'test',
})
export default odmConfigEnvironment Variables
The following environment variables are available for MongoDB configuration (all are optional):
# Connection Settings (Option 1: Use URI)
MONGO_URI=mongodb://localhost:27017/your_database_name
# Connection Settings (Option 2: Use individual components)
MONGO_HOST=localhost
MONGO_PORT=27017
MONGO_DATABASE=your_database_name
# Authentication (optional)
MONGO_USERNAME=your_username
MONGO_PASSWORD=your_password
# Connection Pool Settings (optional)
MONGO_MAX_POOL_SIZE=10
MONGO_MIN_POOL_SIZE=0
MONGO_MAX_IDLE_TIME_MS=30000
MONGO_SERVER_SELECTION_TIMEOUT_MS=5000
MONGO_SOCKET_TIMEOUT_MS=0
MONGO_CONNECT_TIMEOUT_MS=10000Note: All MongoDB connection variables are optional. You can use either MONGO_URI for a complete connection string, or individual components (MONGO_HOST, MONGO_PORT, etc.). The URI takes precedence if both are provided.
Environment Variable Flexibility
As of version 0.2.1+, all MongoDB environment variables are optional, giving you complete flexibility in how you configure your database connection:
- URI Only: Use just
MONGO_URIfor simple setups - Components Only: Use individual variables like
MONGO_HOST,MONGO_PORT, etc. - Mixed: Combine both approaches (URI takes precedence)
- Minimal: Provide only the variables you need
If you're upgrading from an earlier version and experiencing validation errors, see the Migration Guide.
Auto-Connect Configuration
The autoConnect option controls whether the MongoDB provider automatically connects to the database when the application starts. This is particularly useful for testing scenarios:
const odmConfig = defineConfig({
connection: 'mongodb',
connections: {
// ... your connections
},
// Disable auto-connect in test environments
autoConnect: env.get('NODE_ENV') !== 'test',
})Benefits:
- Unit Testing: Run unit tests without requiring a MongoDB server
- CI/CD Pipelines: Tests can run in environments without database access
- Development Flexibility: Control when database connections are established
Default Behavior:
- If
autoConnectis not specified, it defaults totrue - When set to
false, you must manually callawait db.connect()to establish connections - The provider will still register all services and models, just without connecting to MongoDB
Example: Manual Connection
import db from '#services/db'
// Manually connect when needed
await db.connect()
// Your application logic
const users = await User.all()
// Close connections when done
await db.close()Multiple Connections
You can define multiple NoSQL database connections inside the config/odm.ts file and switch between them as needed:
const odmConfig = defineConfig({
connection: 'primary',
connections: {
primary: {
client: 'mongodb',
connection: {
url: env.get('MONGO_PRIMARY_URI'),
},
},
analytics: {
client: 'mongodb',
connection: {
url: env.get('MONGO_ANALYTICS_URI'),
},
},
},
})Note: Database transactions require MongoDB 4.0+ and a replica set or sharded cluster configuration. Transactions are not supported on standalone MongoDB instances.
Commands
The package provides several ace commands to help you work with MongoDB ODM:
Configuration
# Configure the package (run this after installation)
node ace configure adonis-odmModel Generation
# Create a new ODM model
node ace make:odm-model UserDatabase Seeders
# Create a new seeder
node ace make:odm-seeder User
# Create seeder in subdirectory
node ace make:odm-seeder admin/User
# Run all seeders
node ace odm:seed
# Run specific seeder files
node ace odm:seed --files="./database/seeders/user_seeder.ts"
# Run seeders interactively
node ace odm:seed --interactive
# Run seeders for specific connection
node ace odm:seed --connection=analyticsDatabase Operations
# Test database connection (coming soon)
node ace mongodb:status
# Show database information (coming soon)
node ace mongodb:infoDatabase Seeders
Adonis ODM provides a comprehensive seeding system to populate your MongoDB database with initial or test data. The seeder system follows familiar AdonisJS Lucid patterns while providing MongoDB-specific features and advanced execution control.
Quick Start
Creating a Seeder
Generate a new seeder using the ace command:
# Create a basic seeder
node ace make:odm-seeder User
# Create seeder in subdirectory
node ace make:odm-seeder admin/User
# Use different templates
node ace make:odm-seeder User --stub=simple
node ace make:odm-seeder User --stub=advancedThis creates a seeder file in database/seeders/user_seeder.ts:
import { BaseSeeder } from 'adonis-odm/seeders'
import User from '#models/user'
export default class UserSeeder extends BaseSeeder {
async run() {
// Insert seed data
await User.createMany([
{
name: 'John Doe',
email: '[email protected]',
age: 30,
},
{
name: 'Jane Smith',
email: '[email protected]',
age: 28,
},
])
}
}Running Seeders
# Run all seeders
node ace odm:seed
# Run specific seeder files
node ace odm:seed --files="./database/seeders/user_seeder.ts"
# Run seeders interactively (choose which ones to run)
node ace odm:seed --interactive
# Run seeders for specific connection
node ace odm:seed --connection=analyticsAdvanced Features
Environment-Specific Seeders
Control which environments your seeders run in:
import { BaseSeeder } from 'adonis-odm/seeders'
import User from '#models/user'
export default class UserSeeder extends BaseSeeder {
// Only run in development and testing
static environment = ['development', 'testing']
async run() {
await User.createMany([{ name: 'Test User', email: '[email protected]' }])
}
}Custom Execution Order
Control the order in which seeders execute using static properties:
import { BaseSeeder } from 'adonis-odm/seeders'
import Role from '#models/role'
export default class RoleSeeder extends BaseSeeder {
// Lower numbers run first
static order = 1
async run() {
await Role.createMany([
{ name: 'admin', permissions: ['*'] },
{ name: 'user', permissions: ['read'] },
])
}
}Seeder Dependencies
Define dependencies between seeders to ensure proper execution order:
import { BaseSeeder } from 'adonis-odm/seeders'
import User from '#models/user'
export default class UserSeeder extends BaseSeeder {
static order = 2
static dependencies = ['RoleSeeder'] // Must run after RoleSeeder
async run() {
const adminRole = await Role.findBy('name', 'admin')
await User.createMany([
{
name: 'Admin User',
email: '[email protected]',
roleId: adminRole._id,
},
])
}
}Main Seeders
Create main seeder files (index.ts or main.ts) that automatically run first:
// database/seeders/index.ts
import { BaseSeeder } from 'adonis-odm/seeders'
export default class MainSeeder extends BaseSeeder {
// Main seeders automatically get order = 0
async run() {
// Run essential setup logic
console.log('🌱 Starting database seeding...')
}
}Working with Different Data Types
Embedded Documents
Seed models with embedded documents:
import { BaseSeeder } from 'adonis-odm/seeders'
import User from '#models/user'
export default class UserSeeder extends BaseSeeder {
async run() {
await User.createMany([
{
email: '[email protected]',
profile: {
firstName: 'John',
lastName: 'Doe',
bio: 'Software Developer',
age: 30,
},
addresses: [
{
type: 'home',
street: '123 Main St',
city: 'New York',
zipCode: '10001',
},
{
type: 'work',
street: '456 Office Blvd',
city: 'New York',
zipCode: '10002',
},
],
},
])
}
}Referenced Relationships
Seed models with relationships:
import { BaseSeeder } from 'adonis-odm/seeders'
import User from '#models/user'
import Post from '#models/post'
export default class PostSeeder extends BaseSeeder {
static dependencies = ['UserSeeder']
async run() {
const users = await User.all()
for (const user of users) {
await Post.createMany([
{
title: `${user.name}'s First Post`,
content: 'This is my first blog post!',
authorId: user._id,
isPublished: true,
},
{
title: `${user.name}'s Draft`,
content: 'Work in progress...',
authorId: user._id,
isPublished: false,
},
])
}
}
}Connection-Specific Seeding
Use different database connections for different seeders:
import { BaseSeeder } from 'adonis-odm/seeders'
import AnalyticsEvent from '#models/analytics_event'
export default class AnalyticsSeeder extends BaseSeeder {
// Specify connection in the seeder
connection = 'analytics'
async run() {
await AnalyticsEvent.createMany([
{
event: 'user_signup',
userId: 'user123',
timestamp: new Date(),
metadata: { source: 'web' },
},
])
}
}Or specify connection when running:
# Run all seeders on analytics connection
node ace odm:seed --connection=analyticsError Handling and Validation
Seeders include comprehensive error handling:
import { BaseSeeder } from 'adonis-odm/seeders'
import User from '#models/user'
export default class UserSeeder extends BaseSeeder {
async run() {
try {
// Check if data already exists
const existingUsers = await User.query().limit(1)
if (existingUsers.length > 0) {
console.log('Users already exist, skipping seeder')
return
}
await User.createMany([{ name: 'John Doe', email: '[email protected]' }])
console.log('✅ Users seeded successfully')
} catch (error) {
console.error('❌ Error seeding users:', error.message)
throw error // Re-throw to mark seeder as failed
}
}
}Best Practices
- Use Environment Restrictions: Prevent test data from appearing in production
- Define Clear Dependencies: Use
static dependenciesfor complex seeding scenarios - Check for Existing Data: Avoid duplicate data by checking before inserting
- Use Transactions: Wrap complex seeding logic in database transactions
- Provide Feedback: Use console.log to show seeding progress
- Handle Errors Gracefully: Implement proper error handling and cleanup
For more detailed examples and advanced usage patterns, see the seeder documentation and examples.
Usage
Database Service
Import the database service to perform transactions and direct database operations:
import db from 'adonis-odm/services/db'
// Managed transaction (recommended)
const result = await db.transaction(async (trx) => {
// Your operations here
return { success: true }
})
// Manual transaction
const trx = await db.transaction()
try {
// Your operations here
await trx.commit()
} catch (error) {
await trx.rollback()
}
// Direct database access
const mongoClient = db.connection()
const database = db.db()
const collection = db.collection('users')Creating Models
Create a model by extending BaseModel and using decorators:
import { BaseModel, column } from 'adonis-odm'
import { DateTime } from 'luxon'
export default class User extends BaseModel {
@column({ isPrimary: true })
declare _id: string
@column()
declare name: string
@column()
declare email: string
@column()
declare age?: number
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}Collection Naming
The ODM follows the AdonisJS Lucid pattern for collection naming. You can specify custom collection names using the static collection property:
export default class User extends BaseModel {
// Lucid pattern: Use static collection property
static collection = 'custom_users'
@column({ isPrimary: true })
declare _id: string
@column()
declare name: string
}Collection Naming Precedence
The ODM determines collection names in the following order:
- Static collection property (Lucid pattern) - Highest priority
- Metadata tableName (backward compatibility)
- Auto-generated from class name - Default behavior
// 1. Static collection property (recommended)
class User extends BaseModel {
static collection = 'users' // Uses: 'users'
}
// 2. Auto-generated from class name (default)
class AdminUser extends BaseModel {
// Auto-generates: 'admin_users'
}
class APIKey extends BaseModel {
// Auto-generates: 'a_p_i_keys'
}
class UserWithProfile extends BaseModel {
// Auto-generates: 'user_with_profiles'
}Backward Compatibility
The old getCollectionName() method is still supported for backward compatibility:
export default class User extends BaseModel {
// Still works, but static collection property is preferred
static getCollectionName(): string {
return 'users'
}
}Embedded Documents
The ODM provides full support for embedded documents with type safety and CRUD operations.
Defining Embedded Documents
import { BaseModel, column } from 'adonis-odm'
import { DateTime } from 'luxon'
// Embedded document model
export default class Profile extends BaseModel {
@column()
declare firstName: string
@column()
declare lastName: string
@column()
declare bio?: string
@column()
declare age: number
@column()
declare phoneNumber?: string
// Computed property
get fullName(): string {
return `${this.firstName} ${this.lastName}`
}
}
// Import embedded types
import { EmbeddedSingle, EmbeddedMany } from 'adonis-odm'
// Main model with embedded documents
export default class User extends BaseModel {
@column({ isPrimary: true })
declare _id: string
@column()
declare email: string
@column()
declare age: number
// Single embedded document
@column.embedded(() => Profile, 'single')
declare profile?: EmbeddedSingle<typeof Profile>
// Array of embedded documents
@column.embedded(() => Profile, 'many')
declare profiles?: EmbeddedMany<typeof Profile>
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
// Computed properties (using @computed decorator for serialization)
@computed()
get fullName(): string | null {
return this.profile?.fullName || null
}
@computed()
get allProfileNames(): string[] {
return this.profiles?.map((p) => p.fullName) || []
}
// Helper methods (regular methods, not computed properties)
getYoungProfiles(maxAge: number): InstanceType<typeof Profile>[] {
return this.profiles?.filter((p) => p.age < maxAge) || []
}
getProfilesByBio(bioKeyword: string): InstanceType<typeof Profile>[] {
return this.profiles?.filter((p) => p.bio?.includes(bioKeyword)) || []
}
}Creating Records with Embedded Documents
// Create user with embedded profile (single)
const user = await User.create({
email: '[email protected]',
age: 30,
profile: {
firstName: 'John',
lastName: 'Doe',
bio: 'Software developer',
age: 30,
phoneNumber: '+1234567890',
},
})
// Create user with multiple embedded profiles
const user = await User.create({
email: '[email protected]',
age: 28,
profiles: [
{
firstName: 'Jane',
lastName: 'Smith',
bio: 'Technical Lead',
age: 28,
},
{
firstName: 'Jane',
lastName: 'Smith',
bio: 'Architect',
age: 28,
},
],
})Type-Safe Property Access
const user = await User.findOrFail('507f1f77bcf86cd799439011')
// ✅ Full IntelliSense support - NO CASTS NEEDED!
if (user.profile) {
const firstName = user.profile.firstName // ✅ Type: string
const lastName = user.profile.lastName // ✅ Type: string
const bio = user.profile.bio // ✅ Type: string | undefined
const age = user.profile.age // ✅ Type: number
const fullName = user.profile.fullName // ✅ Type: string (computed property)
}
// Array operations with full type safety
if (user.profiles) {
// ✅ Standard array methods work with full type safety
const allBios = user.profiles.map((profile) => profile.bio) // ✅ Type: (string | undefined)[]
const leadProfiles = user.profiles.filter(
(profile) => profile.bio?.includes('Lead') // ✅ Type-safe optional chaining
)
// ✅ Type-safe forEach with IntelliSense
user.profiles.forEach((profile, index) => {
// ✅ Full IntelliSense on profile parameter
console.log(`${index + 1}. ${profile.firstName} ${profile.lastName} - ${profile.bio}`)
})
}CRUD Operations on Embedded Documents
const user = await User.findOrFail('507f1f77bcf86cd799439011')
// Single embedded document operations
if (user.profile) {
// Update properties
user.profile.bio = 'Senior Software Engineer'
user.profile.phoneNumber = '+1-555-9999'
// Save the embedded document
await user.profile.save()
}
// Array embedded document operations
if (user.profiles) {
// Update individual items
const firstProfile = user.profiles[0]
firstProfile.bio = 'Senior Technical Lead'
await firstProfile.save()
// Create new embedded document
const newProfile = user.profiles.create({
firstName: 'John',
lastName: 'Doe',
bio: 'Innovation Lead',
age: 32,
})
await newProfile.save()
// Delete embedded document
await firstProfile.delete()
}Querying Embedded Documents
The ODM provides a powerful query builder for embedded documents with full type safety:
const user = await User.findOrFail('507f1f77bcf86cd799439011')
if (user.profiles) {
// Type-safe query builder with IntelliSense
const seniorProfiles = user.profiles
.query()
.where('bio', 'like', 'Senior') // ✅ Type-safe field names
.where('age', '>=', 30) // ✅ Type-safe operators
.orderBy('age', 'desc') // ✅ Type-safe sorting
.get()
// Complex filtering
const experiencedDevelopers = user.profiles
.query()
.whereAll([
{ field: 'age', operator: '>=', value: 30 },
{ field: 'bio', operator: 'like', value: 'Developer' },
])
.get()
// Pagination for large datasets
const paginatedResult = user.profiles.query().orderBy('age', 'desc').paginate(1, 5) // page 1, 5 per page
console.log(paginatedResult.data) // Array of profiles
console.log(paginatedResult.pagination) // Pagination metadata
// Search across multiple fields
const searchResults = user.profiles.query().search('Engineer', ['bio', 'firstName']).get()
// Aggregation operations
const ageStats = user.profiles.query().aggregate('age')
console.log(ageStats) // { count, sum, avg, min, max }
// Distinct values
const uniqueAges = user.profiles.query().distinct('age')
// Grouping
const ageGroups = user.profiles.query().groupBy('age')
}Loading Embedded Documents with Filtering
Use the .embed() method to load embedded documents with type-safe filtering:
// Load all embedded documents
const users = await User.query().embed('profiles').where('email', 'like', '%@company.com').all()
// Load with filtering callback - Full IntelliSense support!
const users = await User.query()
.embed('profiles', (profileQuery) => {
profileQuery
.where('age', '>', 25) // ✅ Type-safe field names
.where('bio', 'like', 'Engineer') // ✅ Type-safe operators
.orderBy('age', 'desc') // ✅ Type-safe sorting
.limit(5) // ✅ Pagination support
})
.where('email', 'like', '%@company.com')
.all()
// Complex embedded filtering
const users = await User.query()
.embed('profiles', (profileQuery) => {
profileQuery
.whereIn('age', [25, 30, 35])
.whereNotNull('bio')
.whereLike('bio', '%Lead%')
.orderBy('firstName', 'asc')
})
.all()Referenced Relationships
The ODM provides full support for traditional referenced relationships with type-safe decorators and automatic loading.
Defining Referenced Relationships
import { BaseModel, column, hasOne, hasMany, belongsTo } from 'adonis-odm'
import type { HasOne, HasMany, BelongsTo } from 'adonis-odm'
// User model with relationships
export default class User extends BaseModel {
@column({ isPrimary: true })
declare _id: string
@column()
declare name: string
@column()
declare email: string
// One-to-one relationship
@hasOne(() => Profile)
declare profile: HasOne<typeof Profile>
// One-to-many relationship
@hasMany(() => Post)
declare posts: HasMany<typeof Post>
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}
// Profile model with belongs-to relationship
export default class Profile extends BaseModel {
@column({ isPrimary: true })
declare _id: string
@column()
declare firstName: string
@column()
declare lastName: string
@column()
declare userId: string
// Many-to-one relationship
@belongsTo(() => User)
declare user: BelongsTo<typeof User>
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}
// Post model
export default class Post extends BaseModel {
@column({ isPrimary: true })
declare _id: string
@column()
declare title: string
@column()
declare content: string
@column()
declare authorId: string
// Many-to-one relationship
@belongsTo(() => User, { foreignKey: 'authorId' })
declare author: BelongsTo<typeof User>
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}Loading Referenced Relationships
Use the .load() method for type-safe relationship loading:
// Load single relationship
const users = await User.query().load('profile').where('isActive', true).all()
// Load multiple relationships
const users = await User.query().load('profile').load('posts').all()
// Load with filtering callback - Full IntelliSense support!
const users = await User.query()
.load('profile', (profileQuery) => {
profileQuery.where('isPublic', true).orderBy('updatedAt', 'desc')
})
.load('posts', (postQuery) => {
postQuery.where('isPublished', true).orderBy('createdAt', 'desc').limit(5)
})
.all()
// Nested relationship loading
const users = await User.query()
.load('posts', (postQuery) => {
postQuery.load('comments').where('isPublished', true)
})
.all()Working with Loaded Relationships
const user = await User.query().load('profile').load('posts').firstOrFail()
// ✅ Type-safe access with IntelliSense
if (user.profile) {
console.log(user.profile.firstName) // ✅ Type: string
console.log(user.profile.lastName) // ✅ Type: string
}
// ✅ Array relationships with full type safety
if (user.posts) {
user.posts.forEach((post) => {
console.log(post.title) // ✅ Type: string
console.log(post.content) // ✅ Type: string
})
// ✅ Standard array methods work
const publishedPosts = user.posts.filter((post) => post.isPublished)
const postTitles = user.posts.map((post) => post.title)
}Relationship Operations
// Create related models
const user = await User.create({ name: 'John', email: '[email protected]' })
// Create related profile
const profile = await Profile.create({
firstName: 'John',
lastName: 'Doe',
userId: user._id,
})
// Create related posts
const posts = await Post.createMany([
{ title: 'First Post', content: 'Content 1', authorId: user._id },
{ title: 'Second Post', content: 'Content 2', authorId: user._id },
])
// Associate existing models (for belongsTo relationships)
const existingUser = await User.findOrFail('507f1f77bcf86cd799439011')
const newProfile = new Profile()
newProfile.firstName = 'Jane'
newProfile.lastName = 'Smith'
await newProfile.user.associate(existingUser)Basic CRUD Operations
Creating Records
AdonisJS Lucid provides two ways to create records:
Method 1: Using .create() (Recommended)
// Create a single user (no need for 'new')
const user = await User.create({
name: 'John Doe',
email: '[email protected]',
age: 30,
})
// Create multiple users
const users = await User.createMany([
{ name: 'Jane Smith', email: '[email protected]', age: 25 },
{ name: 'Bob Johnson', email: '[email protected]', age: 35 },
])Method 2: Using new + .save()
const user = new User()
// Assign properties
user.name = 'John Doe'
user.email = '[email protected]'
user.age = 30
// Insert to the database
await user.save()Create or Update
const user = await User.updateOrCreate(
{ email: '[email protected]' },
{ name: 'John Doe Updated', age: 32 }
)Reading Records
// Find by ID
const user = await User.find('507f1f77bcf86cd799439011')
const userOrFail = await User.findOrFail('507f1f77bcf86cd799439011')
// Find by field
const user = await User.findBy('email', '[email protected]')
const userOrFail = await User.findByOrFail('email', '[email protected]')
// Get first record
const user = await User.first()
const userOrFail = await User.firstOrFail()
// Get all records
const users = await User.all()Updating Records
AdonisJS Lucid provides three ways to update records:
Method 1: Direct property assignment + save
const user = await User.findOrFail('507f1f77bcf86cd799439011')
user.name = 'Updated Name'
user.age = 31
await user.save()Method 2: Using .merge() + .save() (Method chaining)
const user = await User.findOrFail('507f1f77bcf86cd799439011')
await user.merge({ name: 'Updated Name', age: 31 }).save()Method 3: Using query builder .update() (Bulk update)
// Update multiple records at once
await User.query().where('age', '>=', 18).update({ status: 'adult' })Deleting Records
AdonisJS Lucid provides two ways to delete records:
Method 1: Instance delete
const user = await User.findOrFail('507f1f77bcf86cd799439011')
await user.delete()Method 2: Query builder bulk delete
// Delete multiple records at once
await User.query().where('isVerified', false).delete()Query Builder
The query builder provides a fluent interface for building complex queries:
Basic Queries
// Simple where clause
const adults = await User.query().where('age', '>=', 18).all()
// Multiple conditions
const users = await User.query().where('age', '>=', 18).where('email', 'like', '%@gmail.com').all()
// OR conditions
const users = await User.query().where('age', '>=', 18).orWhere('email', '[email protected]').all()Query Operators
The ODM supports both MongoDB operators and mathematical symbols:
// Mathematical symbols (more intuitive)
User.query().where('age', '>=', 18)
User.query().where('score', '>', 100)
User.query().where('status', '!=', 'inactive')
// MongoDB operators
User.query().where('age', 'gte', 18)
User.query().where('score', 'gt', 100)
User.query().where('status', 'ne', 'inactive')Supported operators:
=,eq- Equal!=,ne- Not equal>,gt- Greater than>=,gte- Greater than or equal<,lt- Less than<=,lte- Less than or equalin- In arraynin- Not in arrayexists- Field existsregex- Regular expressionlike- Pattern matching with % wildcards
Advanced Queries
// Null checks
const users = await User.query().whereNull('deletedAt').all()
const users = await User.query().whereNotNull('emailVerifiedAt').all()
// In/Not in arrays
const users = await User.query().whereIn('status', ['active', 'pending']).all()
const users = await User.query().whereNotIn('role', ['admin', 'moderator']).all()
// Between values
const users = await User.query().whereBetween('age', [18, 65]).all()
const users = await User.query().whereNotBetween('age', [13, 17]).all()
// Pattern matching with like
const users = await User.query().where('name', 'like', 'John%').all()
const users = await User.query().whereLike('name', 'John%').all() // Case-sensitive
const users = await User.query().whereILike('name', 'john%').all() // Case-insensitive
// Field existence
const users = await User.query().whereExists('profilePicture').all()
const users = await User.query().whereNotExists('deletedAt').all()
// Negation queries
const users = await User.query().whereNot('status', 'banned').all()
const users = await User.query().whereNot('age', '<', 18).all()
// Complex OR conditions
const users = await User.query()
.where('role', 'admin')
.orWhere('permissions', 'like', '%manage%')
.orWhereIn('department', ['IT', 'Security'])
.orWhereNotNull('specialAccess')
.all()
// Alias methods for clarity
const users = await User.query()
.where('age', '>=', 18)
.andWhere('status', 'active') // Same as .where()
.andWhereNot('role', 'guest') // Same as .whereNot()
.all()
// Sorting
const users = await User.query().orderBy('createdAt', 'desc').orderBy('name', 'asc').all()
// Limiting and pagination
const users = await User.query().limit(10).skip(20).all()
const users = await User.query().offset(20).limit(10).all() // offset is alias for skip
const users = await User.query().forPage(3, 10).all() // page 3, 10 per page
// Field selection
const users = await User.query().select(['name', 'email']).all()
// Distinct values
const uniqueRoles = await User.query().distinct('role').all()
// Grouping and aggregation
const departmentStats = await User.query().groupBy('department').having('count', '>=', 5).all()
// Query cloning
const baseQuery = User.query().where('status', 'active')
const adminQuery = baseQuery.clone().where('role', 'admin')
const userQuery = baseQuery.clone().where('role', 'user')Pagination
const paginatedUsers = await User.query().orderBy('createdAt', 'desc').paginate(1, 10) // page 1, 10 per page
console.log(paginatedUsers.data) // Array of users
console.log(paginatedUsers.meta) // Pagination metadataAggregation
// Count records
const userCount = await User.query().where('age', '>=', 18).count()
// Get IDs only
const userIds = await User.query().where('status', 'active').ids()
// Delete multiple records
const deletedCount = await User.query().where('status', 'inactive').delete()
// Update multiple records
const updatedCount = await User.query().where('age', '>=', 18).update({ status: 'adult' })Column Decorators
The ODM provides several decorators for defining model properties and their behavior.
Basic Column
@column()
declare name: string
@column({ isPrimary: true })
declare _id: stringEmbedded Columns
// Single embedded document
@column.embedded(() => Profile, 'single')
declare profile?: EmbeddedSingle<typeof Profile>
// Array of embedded documents
@column.embedded(() => Profile, 'many')
declare profiles?: EmbeddedMany<typeof Profile>Date Columns
// Auto-create timestamp (set only on creation)
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
// Auto-update timestamp (set on creation and updates)
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
// Custom date column
@column.date()
declare birthDate: DateTimeDecimal Columns
For precise decimal arithmetic and financial data, use the @column.decimal() decorator to properly handle MongoDB's Decimal128 type:
@column.decimal()
declare price: number
@column.decimal()
declare earnings: number
@column.decimal()
declare taxAmount: numberWhy use @column.decimal()?
Without the decimal decorator, MongoDB decimal values are serialized as objects like { "$numberDecimal": "100.99" } instead of proper numbers. The decimal decorator:
- Stores values as MongoDB Decimal128 for precision
- Deserializes to JavaScript numbers for calculations
- Serializes to proper numbers in JSON responses
- Handles both
Decimal128and{ $numberDecimal: "..." }formats from MongoDB
Custom Serialization
@column({
serialize: (value) => value.toUpperCase(),
deserialize: (value) => value.toLowerCase(),
})
declare name: stringComputed Properties
Computed properties are getter-only properties that are calculated from other model attributes. They are included in JSON serialization but excluded from database operations.
import { BaseModel, column, computed } from 'adonis-odm'
import { DateTime } from 'luxon'
export default class User extends BaseModel {
@column({ isPrimary: true })
declare _id: string
@column()
declare firstName: string
@column()
declare lastName: string
@column()
declare email: string
@column()
declare salary: number
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
// Basic computed property
@computed()
get fullName(): string {
return `${this.firstName} ${this.lastName}`
}
// Computed property with custom serialization name
@computed({ serializeAs: 'display_name' })
get displayName(): string {
return `${this.firstName} ${this.lastName}`.toUpperCase()
}
// Computed property that won't be serialized
@computed({ serializeAs: null })
get internalCalculation(): number {
return this.salary * 0.1 // This won't appear in JSON output
}
// Complex computed property
@computed()
get profileSummary(): string {
const yearsActive = DateTime.now().diff(this.createdAt, 'years').years
return `${this.fullName} (${Math.floor(yearsActive)} years active)`
}
// Computed property based on relationships
@computed()
get hasProfile(): boolean {
return this.profile !== undefined && this.profile !== null
}
}Using Computed Properties
const user = await User.create({
firstName: 'John',
lastName: 'Doe',
email: '[email protected]',
salary: 50000,
})
// Access computed properties directly
console.log(user.fullName) // "John Doe"
console.log(user.displayName) // "JOHN DOE"
console.log(user.profileSummary) // "John Doe (0 years active)"
// Computed properties are included in JSON serialization
const json = user.toJSON()
console.log(json)
// Output:
// {
// _id: "...",
// first_name: "John",
// last_name: "Doe",
// email: "[email protected]",
// salary: 50000,
// created_at: "2024-01-01T00:00:00.000Z",
// updated_at: "2024-01-01T00:00:00.000Z",
// full_name: "John Doe",
// display_name: "JOHN DOE",
// profile_summary: "John Doe (0 years active)",
// has_profile: false
// // Note: internal_calculation is not included (serializeAs: null)
// }
// Computed properties are NOT included in database operations
await user.save() // Only saves actual column data, not computed propertiesComputed Properties with Relationships
export default class User extends BaseModel {
@column({ isPrimary: true })
declare _id: string
@column()
declare name: string
@hasOne(() => Profile)
declare profile: HasOne<typeof Profile>
@hasMany(() => Post)
declare posts: HasMany<typeof Post>
// Computed property from loaded relationship
@computed()
get fullName(): string {
return this.profile?.fullName ?? this.name
}
// Computed property with relationship data
@computed()
get postCount(): number {
return this.posts?.length ?? 0
}
// Complex computed property
@computed()
get userStats(): object {
return {
name: this.name,
hasProfile: !!this.profile,
totalPosts: this.postCount,
joinedDate: this.createdAt.toFormat('yyyy-MM-dd'),
}
}
}
// Usage with loaded relationships
const user = await User.query().load('profile').load('posts').firstOrFail()
console.log(user.fullName) // Uses profile data if available
console.log(user.postCount) // Returns actual post count
console.log(user.userStats) // Complex computed objectWhen to Use @computed() vs Regular Getters
Use @computed() decorator when:
- You want the property included in JSON serialization
- You need custom serialization names (
serializeAs) - You want to exclude from serialization (
serializeAs: null) - The property represents computed data that should be part of the model's public API
Use regular getters when:
- You want simple helper methods that don't need serialization
- The getter is for internal use only
- You're working with embedded documents where serialization is handled differently
export default class User extends BaseModel {
@column()
declare firstName: string
@column()
declare lastName: string
// ✅ Use @computed() for serialized properties
@computed()
get fullName(): string {
return `${this.firstName} ${this.lastName}`
}
// ✅ Use regular getter for internal helpers
get initials(): string {
return `${this.firstName[0]}${this.lastName[0]}`
}
// ✅ Use @computed() with custom serialization
@computed({ serializeAs: 'display_name' })
get displayName(): string {
return this.fullName.toUpperCase()
}
// ✅ Use @computed() to exclude from serialization
@computed({ serializeAs: null })
get internalId(): string {
return `internal_${this._id}`
}
}Computed Properties Best Practices
- Keep computations lightweight - Avoid heavy calculations in getters
- Use appropriate return types - TypeScript will infer types automatically
- Handle null/undefined cases - Always check for loaded relationships
- Use meaningful names - Make computed property names descriptive
- Consider serialization - Use
serializeAsto control JSON output - Avoid side effects - Computed properties should be pure functions
- Choose the right pattern - Use
@computed()for serialized properties, regular getters for helpers
Model Lifecycle
Models track their state automatically:
const user = new User({ name: 'John' })
console.log(user.$isLocal) // true
console.log(user.$isPersisted) // false
await user.save()
console.log(user.$isLocal) // false
console.log(user.$isPersisted) // true
user.name = 'Jane'
console.log(user.$dirty) // { name: 'Jane' }Lifecycle Hooks
The ODM provides a comprehensive hook system that allows you to execute custom logic at various points in the model lifecycle. Hooks are defined using decorators and are executed automatically.
Available Hooks
import {
BaseModel,
column,
beforeSave,
afterSave,
beforeCreate,
afterCreate,
beforeUpdate,
afterUpdate,
beforeDelete,
afterDelete,
beforeFind,
afterFind,
beforeFetch,
afterFetch,
} from 'adonis-odm'
export default class User extends BaseModel {
@column({ isPrimary: true })
declare _id: string
@column()
declare name: string
@column()
declare email: string
@column()
declare password: string
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
// Hooks that run before/after save operations (create and update)
@beforeSave()
static async hashPassword(user: User) {
if (user.$dirty.password) {
user.password = await hash(user.password)
}
}
@afterSave()
static async logSave(user: User) {
console.log(`User ${user.name} was saved`)
}
// Hooks that run before/after create operations
@beforeCreate()
static async validateEmail(user: User) {
const existingUser = await User.findBy('email', user.email)
if (existingUser) {
throw new Error('Email already exists')
}
}
@afterCreate()
static async sendWelcomeEmail(user: User) {
// Send welcome email logic
console.log(`Welcome email sent to ${user.email}`)
}
// Hooks that run before/after update operations
@beforeUpdate()
static async validateUpdate(user: User) {
if (user.$dirty.email) {
// Validate email change
console.log('Email is being changed')
}
}
@afterUpdate()
static async logUpdate(user: User) {
console.log(`User ${user.name} was updated`)
}
// Hooks that run before/after delete operations
@beforeDelete()
static async checkDependencies(user: User) {
const posts = await Post.query().where('authorId', user._id).count()
if (posts > 0) {
throw new Error('Cannot delete user with existing posts')
}
}
@afterDelete()
static async cleanup(user: User) {
// Cleanup related data
console.log(`Cleanup completed for user ${user.name}`)
}
// Hooks that run before/after find operations
@beforeFind()
static async logFind(query: ModelQueryBuilder<any, User>) {
console.log('Finding user...')
}
@afterFind()
static async logFoundUser(user: User | null) {
if (user) {
console.log(`Found user: ${user.name}`)
}
}
// Hooks that run before/after fetch operations (multiple records)
@beforeFetch()
static async logFetch(query: ModelQueryBuilder<any, User>) {
console.log('Fetching users...')
}
@afterFetch()
static async logFetchedUsers(users: User[]) {
console.log(`Fetched ${users.length} users`)
}
}Hook Execution Order
Hooks are executed in the following order:
For Create Operations:
beforeSavebeforeCreate- Database operation
afterCreateafterSave
For Update Operations:
beforeSavebeforeUpdate- Database operation
afterUpdateafterSave
For Delete Operations:
beforeDelete- Database operation
afterDelete
For Find Operations:
beforeFind- Database operation
afterFind
For Fetch Operations:
beforeFetch- Database operation
afterFetch
Aborting Operations
Before hooks can abort operations by returning false:
export default class User extends BaseModel {
@beforeSave()
static async validateUser(user: User) {
if (!user.email.includes('@')) {
console.log('Invalid email format')
return false // Aborts the save operation
}
}
@beforeDelete()
static async preventAdminDeletion(user: User) {
if (user.role === 'admin') {
console.log('Cannot delete admin user')
return false // Aborts the delete operation
}
}
}Hook Best Practices
- Keep hooks lightweight - Avoid heavy computations in hooks
- Use async/await - Hooks support asynchronous operations
- Handle errors gracefully - Use try/catch blocks for error handling
- Return false to abort - Use return false in before hooks to prevent operations
- Use appropriate hook types - Choose the right hook for your use case
Database Transactions
The MongoDB ODM provides full ACID transaction support, similar to AdonisJS Lucid ORM. Transactions ensure that multiple database operations are executed atomically - either all operations succeed, or all are rolled back.
Managed Transactions (Recommended)
Managed transactions automatically handle commit and rollback operations:
import db from 'adonis-odm/services/db'
// Managed transaction with automatic commit/rollback
const newUser = await db.transaction(async (trx) => {
// Create user within transaction
const user = await User.create(
{
name: 'John Doe',
email: '[email protected]',
},
{ client: trx }
)
// Create related profile within same transaction
const profile = await Profile.create(
{
userId: user._id,
firstName: 'John',
lastName: 'Doe',
},
{ client: trx }
)
// If any operation fails, entire transaction is rolled back
// If all operations succeed, transaction is automatically committed
return user
})
console.log('Transaction completed successfully:', newUser.toJSON())Manual Transactions
For more control, you can manually manage transaction lifecycle:
// Manual transaction with explicit commit/rollback
const trx = await db.transaction()
try {
// Create user within transaction
const user = await User.create(
{
name: 'Jane Smith',
email: '[email protected]',
},
{ client: trx }
)
// Update user within transaction
await User.query({ client: trx }).where('_id', user._id).update({ age: 30 })
// Manually commit the transaction
await trx.commit()
console.log('Transaction committed successfully')
} catch (error) {
// Manually rollback on error
await trx.rollback()
console.error('Transaction rolled back:', error)
}Model Instance Transactions
You can associate model instances with transactions:
await db.transaction(async (trx) => {
const user = new User()
user.name = 'Bob Johnson'
user.email = '[email protected]'
// Associate model with transaction
user.useTransaction(trx)
await user.save()
// Update the same instance
user.age = 35
await user.save() // Uses the same transaction
})Query Builder with Transactions
All query builder operations support transactions:
const trx = await db.transaction()
try {
// Query with transaction
const users = await User.query({ client: trx }).where('isActive', true).all()
// Update multiple records
const updateCount = await User.query({ client: trx })
.where('age', '>=', 18)
.update({ status: 'adult' })
// Delete records
const deleteCount = await User.query({ client: trx }).where('isVerified', false).delete()
await trx.commit()
} catch (error) {
await trx.rollback()
throw error
}Transaction Options
You can pass MongoDB-specific transaction options:
// With transaction options
const result = await db.transaction(
async (trx) => {
// Your operations here
return await User.create({ name: 'Test' }, { client: trx })
},
{
readConcern: { level: 'majority' },
writeConcern: { w: 'majority' },
readPreference: 'primary',
}
)
// Manual transaction with options
const trx = await db.transaction({
readConcern: { level: 'majority' },
writeConcern: { w: 'majority' },
})Error Handling and Rollback
Transactions automatically rollback on errors:
try {
await db.transaction(async (trx) => {
await User.create({ name: 'Test User' }, { client: trx })
// This will cause the entire transaction to rollback
throw new Error('Something went wrong')
})
} catch (error) {
console.log('Transaction was automatically rolled back')
// The user creation above was not persisted
}Best Practices
- Use managed transactions when possible for automatic error handling
- Keep transactions short to minimize lock time
- Handle errors appropriately and always rollback on failure
- Use transactions for related operations that must succeed or fail together
- Pass transaction client to all operations that should be part of the transaction
Connection Management
You can work with multiple MongoDB connections:
// In your model
export default class User extends BaseModel {
static getConnection(): string {
return 'secondary' // Use a different connection
}
}
// Using different connections in queries
const primaryUsers = await User.query().all() // Uses default connection
const analyticsUsers = await User.query({ connection: 'analytics' }).all() // Uses analytics connection
// Direct database access with specific connections
const primaryDb = db.connection('primary')
const analyticsDb = db.connection('analytics')Error Handling
The ODM provides comprehensive error handling with custom exception types for different scenarios.
Exception Types
import {
MongoOdmException,
ModelNotFoundException,
ConnectionException,
DatabaseOperationException,
ValidationException,
TransactionException,
} from 'adonis-odm'
// Base exception for all ODM errors
try {
// ODM operations
} catch (error) {
if (error instanceof MongoOdmException) {
console.log('ODM Error:', error.message)
}
}
// Model not found exception
try {
const user = await User.findOrFail('invalid-id')
} catch (error) {
if (error instanceof ModelNotFoundException) {
console.log('User not found:', error.message)
// Error message: "User with identifier "invalid-id" not found"
}
}
// Connection exception
try {
await db.connect()
} catch (error) {
if (error instanceof ConnectionException) {
console.log('Connection failed:', error.message)
// Error message: "Failed to connect to MongoDB connection "primary": ..."
}
}
// Database operation exception
try {
await User.query().where('invalid.field', 'value').all()
} catch (error) {
if (error instanceof DatabaseOperationException) {
console.log('Database operation failed:', error.message)
// Error message: "Database operation "find" failed: ..."
}
}
// Validation exception
try {
const user = new User()
user.email = 'invalid-email'
await user.save()
} catch (error) {
if (error instanceof ValidationException) {
console.log('Validation failed:', error.message)
// Error message: "Validation failed for field "email" with value "invalid-email": must be a valid email"
}
}
// Transaction exception
try {
await db.transaction(async (trx) => {
// Transaction operations that fail
throw new Error('Something went wrong')
})
} catch (error) {
if (error instanceof TransactionException) {
console.log('Transaction failed:', error.message)
// Error message: "Transaction operation "commit" failed: ..."
}
}Error Handling Best Practices
// 1. Use specific exception types for targeted error handling
export default class UserController {
async show({ params, response }: HttpContext) {
try {
const user = await User.findOrFail(params.id)
return user
} catch (error) {
if (error instanceof ModelNotFoundException) {
return response.status(404).json({ error: 'User not found' })
}
throw error // Re-throw other errors
}
}
async store({ request, response }: HttpContext) {
try {
const userData = request.only(['name', 'email'])
const user = await User.create(userData)
return response.status(201).json(user)
} catch (error) {
if (error instanceof ValidationException) {
return response.status(422).json({ error: error.message })
}
if (error instanceof DatabaseOperationException) {
return response.status(500).json({ error: 'Database error occurred' })
}
throw error
}
}
}
// 2. Use global exception handler for consistent error responses
export default class HttpExceptionHandler extends ExceptionHandler {
async handle(error: unknown, ctx: HttpContext) {
if (error instanceof ModelNotFoundException) {
return ctx.response.status(404).json({
error: 'Resource not found',
message: error.message,
})
}
if (error instanceof ValidationException) {
return ctx.response.status(422).json({
error: 'Validation failed',
message: error.message,
})
}
if (error instanceof ConnectionException) {
return ctx.response.status(503).json({
error: 'Service unavailable',
message: 'Database connection failed',
})
}
return super.handle(error, ctx)
}
}
// 3. Graceful error handling in transactions
async function transferData() {
try {
await db.transaction(async (trx) => {
const user = await User.create({ name: 'John' }, { client: trx })
const profile = await Profile.create({ userId: user._id }, { client: trx })
// If any operation fails, transaction is automatically rolled back
return { user, profile }
})
} catch (error) {
if (error instanceof TransactionException) {
console.log('Transaction failed and was rolled back')
}
// Handle other errors
}
}Performance & Advanced Features
Bulk Operations
The ODM supports efficient bulk operations for better performance:
// Bulk create
const users = await User.createMany([
{ name: 'User 1', email: '[email protected]' },
{ name: 'User 2', email: '[email protected]' },
{ name: 'User 3', email: '[email protected]' },
])
// Bulk update
const updateCount = await User.query().where('isActive', false).update({ status: 'inactive' })
// Bulk delete
const deleteCount = await User.query()
.where('lastLoginAt', '<', DateTime.now().minus({ months: 6 }))
.delete()
// Bulk upsert (update or create)
const results = await User.updateOrCreateMany(
'email', // Key field
[
{ email: '[email protected]', name: 'Updated User 1' },
{ email: '[email protected]', name: 'New User 4' },
]
)Connection Pooling
MongoDB connection pooling is automatically configured for optimal performance:
// Configure connection pool in config/odm.ts
const odmConfig = defineConfig({
connections: {
mongodb: {
client: 'mongodb',
connection: {
url: env.get('MONGO_URI'),
options: {
maxPoolSize: 20, // Maximum connections in pool
minPoolSize: 5, // Minimum connections in pool
maxIdleTimeMS: 30000, // Close connections after 30s idle
serverSelectionTimeoutMS: 5000, // Timeout for server selection
socketTimeoutMS: 0, // No socket timeout
connectTimeoutMS: 10000, // 10s connection timeout
},
},
},
},
})Query Optimization
// Use indexes for better query performance
const users = await User.query()
.where('email', '[email protected]') // Ensure email field is indexed
.where('isActive', true) // Compound index on email + isActive
.first()
// Limit fields to reduce data transfer
const users = await User.query()
.select(['name', 'email']) // Only fetch required fields
.where('isActive', true)
.all()
// Use pagination for large datasets
const paginatedUsers = await User.query()
.where('isActive', true)
.orderBy('createdAt', 'desc')
.paginate(1, 50) // Page 1, 50 records per page
// Efficient counting
const activeUserCount = await User.query().where('isActive', true).count() // More efficient than fetching all recordsRelationship Loading Optimization
// Eager load relationships to prevent N+1 queries
const users = await User.query()
.load('profile')
.load('posts', (postQuery) => {
postQuery.limit(5).orderBy('createdAt', 'desc')
})
.where('isActive', true)
.all()
// Bulk load relationships for multiple models
const userIds = ['id1', 'id2', 'id3']
const users = await User.query().whereIn('_id', userIds).load('profile').all()Embedded Document Performance
// Efficient embedded document queries
const users = await User.query()
.embed('profiles', (profileQuery) => {
profileQuery.where('age', '>', 25).orderBy('age', 'desc').limit(3) // Limit embedded results
})
.where('isActive', true)
.all()
// Aggregation on embedded documents
const userStats = await User.query()
.where('profiles.age', '>', 18)
.aggregate([
{ $unwind: '$profiles' },
{ $group: { _id: null, avgAge: { $avg: '$profiles.age' } } },
])Caching Strategies
// Model-level caching (implement in your application)
class CachedUser extends User {
static async findCached(id: string): Promise<User | null> {
const cacheKey = `user:${id}`
let user = await cache.get(cacheKey)
if (!user) {
user = await this.find(id)
if (user) {
await cache.set(cacheKey, user, { ttl: 300 }) // 5 minutes
}
}
return user
}
}
// Query result caching
const cacheKey = 'active-users'
let activeUsers = await cache.get(cacheKey)
if (!activeUsers) {
activeUsers = await User.query().where('isActive', true).all()
await cache.set(cacheKey, activeUsers, { ttl: 60 }) // 1 minute
}Advanced Query Patterns
// Complex aggregation pipelines
const userStats = await User.aggregate([
{ $match: { isActive: true } },
{
$group: {
_id: '$department',
count: { $sum: 1 },
avgAge: { $avg: '$age' },
maxSalary: { $max: '$salary' },
},
},
{ $sort: { count: -1 } },
])
// Geospatial queries (if using location data)
const nearbyUsers = await User.query()
.where('location', 'near', {
geometry: { type: 'Point', coordinates: [longitude, latitude] },
maxDistance: 1000, // meters
})
.all()
// Text search
const searchResults = await User.query()
.where('$text', { $search: 'john developer' })
.orderBy({ score: { $meta: 'textScore' } })
.all()
// Complex filtering with $expr
const users = await User.query()
.where('$expr', {
$gt: [{ $size: '$posts' }, 10], // Users with more than 10 posts
})
.all()Memory Management
// Use streams for large datasets
const userStream = User.query().where('isActive', true).stream()
userStream.on('data', (user) => {
// Process each user individually
processUser(user)
})
userStream.on('end', () => {
console.log('Finished processing all users')
})
// Cursor-based pagination for large datasets
let cursor = null
const batchSize = 1000
do {
const query = User.query().limit(batchSize)
if (cursor) {
query.where('_id', '>', cursor)
}
const users = await query.orderBy('_id').all()
if (users.length > 0) {
cursor = users[users.length - 1]._id
await processBatch(users)
}
if (users.length < batchSize) {
break // No more data
}
} while (true)API Reference
BaseModel
Static Methods
query(options?)- Create a new query builderfind(id, options?)- Find by IDfindOrFail(id, options?)- Find by ID or throwfindBy(field, value)- Find by fieldfindByOrFail(field, value)- Find by field or throwfirst()- Get first recordfirstOrFail()- Get first record or throwall()- Get all recordscreate(attributes, options?)- Create new recordcreateMany(attributesArray)- Create multiple recordsupdateOrCreate(search, update)- Update existing or create new
Instance Methods
save()- Save the modeldelete()- Delete the modelfill(attributes)- Fill with attributesmerge(attributes)- Merge attributestoDocument()- Convert to plain objectuseTransaction(trx)- Associate model with transaction
Instance Properties
$isPersisted- Whether the model exists in database$isLocal- Whether the model is local only$dirty- Object containing modified attributes$original- Original values before modifications$trx- Associated transaction client (if any)
Column Decorators
@column(options?)
Define a regular database column.
Options:
isPrimary?: boolean- Mark as primary keyserialize?: (value: any) => any- Custom serialization functiondeserialize?: (value: any) => any- Custom deserialization functionserializeAs?: string | null- Custom JSON key name
@column.dateTime(options?)
Define a DateTime column with automatic timestamp handling.
Options:
autoCreate?: boolean- Set timestamp on creationautoUpdate?: boolean- Update timestamp on saveserialize?: (value: DateTime) => any- Custom serializationdeserialize?: (value: any) => DateTime- Custom deserialization
@column.date(options?)
Define a Date column.
@column.embedded(model, type, options?)
Define an emb
