ORM Patterns
Hysteria ORM supports two distinct patterns for working with your database models:
- Active Record Pattern - Static methods on model classes (recommended)
- Repository Pattern - Using
getModelManager()for dependency injection
Active Record Pattern
The Active Record pattern is the recommended approach for most applications. It provides a clean, intuitive API where you call static methods directly on your model classes.
How it Works
In the Active Record pattern, your model classes have static methods that handle database operations. These methods internally use a ModelManager instance but abstract away the complexity.
Example Usage
import { Model, column } from 'hysteria-orm';
class User extends Model {
@column.integer({ primaryKey: true })
declare id: number;
@column()
declare name: string;
@column()
declare email: string;
}
// Active Record Pattern - Static methods on the model
const user = await User.insert({ name: 'John', email: 'john@example.com' });
const users = await User.find({ where: { isActive: true } });
const userById = await User.findOneByPrimaryKey(1);
const query = User.query().where('name', 'like', '%John%').many();
Available Static Methods
| Method | Description | Example |
|---|---|---|
Model.insert(data) | Insert a new record | User.insert({ name: 'John' }) |
Model.insertMany(data[]) | Insert multiple records | User.insertMany([{ name: 'John' }, { name: 'Jane' }]) |
Model.find(options) | Find multiple records | User.find({ where: { isActive: true } }) |
Model.findOne(options) | Find a single record | User.findOne({ where: { email: 'john@example.com' } }) |
Model.findBy(column, value) | Find by specific column | User.findBy('email', 'john@example.com') |
Model.findOneBy(column, value) | Find one by specific column | User.findOneBy('email', 'john@example.com') |
Model.findOneByPrimaryKey(id) | Find by primary key | User.findOneByPrimaryKey(1) |
Model.update(data, options) | Update records | User.update({ name: 'Johnny' }, { where: { id: 1 } }) |
Model.delete(options) | Delete records | User.delete({ where: { isActive: false } }) |
Model.query() | Get query builder | User.query().where('age', '>', 18).many() |
Model.all() | Get all records | User.all() |
Model.count(options) | Count records | User.count({ where: { isActive: true } }) |
Model.exists(options) | Check if records exist | User.exists({ where: { email: 'john@example.com' } }) |
Benefits
- Clean API: Simple, intuitive method calls
- Type Safety: Full TypeScript support with proper typing
- Model Awareness: Automatic handling of decorators, relations, and hooks
- Less Boilerplate: No need to manage ModelManager instances
- Familiar: Similar to other popular ORMs like TypeORM
- Model Embedding Support: Can be used with embedded models for even cleaner syntax
Model Embedding with Active Record
Model embedding allows you to attach models directly to a SQL data source instance, providing an even cleaner API:
const sql = new SqlDataSource({
type: 'postgres',
models: { user: User, post: Post },
});
await sql.connect();
// Access models directly through the data source
const user = await sql.user.insert({ name: 'John', email: 'john@example.com' });
const users = await sql.user.find({ where: { isActive: true } });
For detailed documentation, see Model Embedding.
Repository Pattern (getModelManager)
The Repository pattern provides more control and is useful for dependency injection, testing, or when you need explicit control over the data source connection.
How it Works
Instead of using static methods, you get a ModelManager instance from the data source and use its methods directly.
Example Usage
import { SqlDataSource, Model, column } from 'hysteria-orm';
class User extends Model {
@column.integer({ primaryKey: true })
declare id: number;
@column()
declare name: string;
@column()
declare email: string;
}
// Connect first
const sql = new SqlDataSource({ type: 'postgres', ... });
await sql.connect();
// Repository Pattern - Using getModelManager
const userManager = sql.getModelManager(User);
const user = await userManager.insert({ name: 'John', email: 'john@example.com' });
const users = await userManager.find({ where: { isActive: true } });
const userById = await userManager.findOneByPrimaryKey(1);
const query = userManager.query().where('name', 'like', '%John%').many();
When to Use Repository Pattern
The Repository pattern is useful when you need:
- Dependency Injection: Pass the manager to services or controllers
- Testing: Mock the manager for unit tests
- Multiple Connections: Use different data sources for the same model
- Explicit Control: Want to manage the data source connection explicitly
Example with Dependency Injection
// Service class that accepts a ModelManager
class UserService {
constructor(private userManager: ModelManager<User>) {}
async createUser(userData: Partial<User>) {
return await this.userManager.insert(userData);
}
async findActiveUsers() {
return await this.userManager.find({
where: { isActive: true }
});
}
}
// Usage
const userManager = sql.getModelManager(User);
const userService = new UserService(userManager);
const users = await userService.findActiveUsers();
Example with Custom Connection
// Using a secondary connection
const secondarySql = await SqlDataSource.connectToSecondarySource({
type: 'postgres',
host: 'read-replica.example.com',
// ... other config
});
const readOnlyUserManager = secondarySql.getModelManager(User);
const users = await readOnlyUserManager.find({ where: { isActive: true } });
Pattern Comparison
| Feature | Active Record | Model Embedding | Repository Pattern |
|---|---|---|---|
| API Simplicity | ✅ Simple | ✅ Cleanest | ⚠️ Verbose |
| Type Safety | ✅ Full | ✅ Full | ✅ Full |
| Dependency Injection | ❌ Limited | ✅ Perfect | ✅ Perfect |
| Testing | ⚠️ Harder | ✅ Easy | ✅ Easy |
| Multiple Connections | ⚠️ Limited | ✅ Full | ✅ Full |
When to Use Each Pattern
| Pattern | Best For |
|---|---|
| Active Record | Simple apps, quick prototypes, default connection |
| Model Embedding | Service architecture, dependency injection, microservices |
| Repository Pattern | Enterprise apps, fine-grained control, multiple connections |
Migrating Between Patterns
All patterns use the same underlying ModelManager, so you can switch as your application grows:
// Start with Active Record
const user = await User.insert({ name: 'John' });
// Upgrade to Model Embedding
const sql = new SqlDataSource({
type: 'postgres',
models: { user: User },
});
const user = await sql.user.insert({ name: 'John' });
// Or use Repository Pattern
const userManager = sql.getModelManager(User);
const user = await userManager.insert({ name: 'John' });
Next: Defining Models