ORM Patterns
Hysteria ORM supports three distinct patterns for working with your database models:
- Query Builder Pattern — Using
sql.from(Model)directly (recommended) - Model Manager Pattern — Using
sql.getModelManager(Model)for DI/testing - Model Embedding Pattern — Using embedded models on the
SqlDataSourceinstance
Defining a Model
All patterns use defineModel and col to declare models — no class-based decorators needed:
import { SqlDataSource, defineModel, col } from "hysteria-orm";
const User = defineModel("users", {
columns: {
id: col.increment(),
name: col.string(),
email: col.string(),
isActive: col.boolean().default(true),
},
});
Connecting to the Database
SqlDataSource is not a singleton. You create and manage instances explicitly:
const sql = new SqlDataSource({ type: "sqlite", database: "app.db" });
await sql.connect();
Query Builder Pattern (Recommended)
The Query Builder pattern is the recommended approach for most applications. Call sql.from(Model) to get a fully typed query builder for any model.
Example Usage
// Insert
const user = await sql
.from(User)
.insert({ name: "John", email: "john@example.com" });
// Find with conditions
const users = await sql.from(User).find({ where: { isActive: true } });
// Find by primary key
const userById = await sql.from(User).findOneByPrimaryKey(1);
// Query builder chain
const results = await sql.from(User).where("name", "like", "%John%").many();
Benefits
- Clean API: Simple, intuitive method calls
- Type Safety: Full TypeScript support with proper typing
- Explicit Connection: Always clear which data source is being used
- Multiple Connections: Easily use different data sources for the same model
- No Singletons: No hidden global state
For the full list of available methods (insert, find, update, delete, upsert, etc.), see CRUD Operations.
Model Manager Pattern
The Model Manager pattern provides a reusable handle to a model's operations. It is useful for dependency injection, testing, or when you want to pass a model-bound manager around your application.
Example Usage
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 results = await userManager
.from()
.where("name", "like", "%John%")
.many();
When to Use Model Manager Pattern
The Model Manager 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
import { ModelManager } from "hysteria-orm";
class UserService {
constructor(private userManager: ModelManager<typeof User>) {}
async createUser(userData: { name: string; email: string }) {
return this.userManager.insert(userData);
}
async findActiveUsers() {
return this.userManager.find({
where: { isActive: true },
});
}
}
// Usage
const userManager = sql.getModelManager(User);
const userService = new UserService(userManager);
const users = await userService.findActiveUsers();
Model Embedding Pattern
The Model Embedding pattern attaches models directly to the SqlDataSource instance, providing the most concise syntax.
Example Usage
const sqlWithModels = new SqlDataSource({
type: "sqlite",
database: "app.db",
models: { user: User },
});
await sqlWithModels.connect();
// Access models directly on the instance
const user = await sqlWithModels.user.insert({
name: "John",
email: "john@example.com",
});
const users = await sqlWithModels.user.find({ where: { isActive: true } });
const userById = await sqlWithModels.user.findOneByPrimaryKey(1);
For full documentation, see Model Embedding.
Using Multiple Connections
Since SqlDataSource is not a singleton, you simply create additional instances for read replicas or other databases:
const primary = new SqlDataSource({
type: "postgres",
host: "primary.example.com",
database: "app",
});
await primary.connect();
const readReplica = new SqlDataSource({
type: "postgres",
host: "read-replica.example.com",
database: "app",
});
await readReplica.connect();
// Write to primary
await primary.from(User).insert({ name: "John", email: "john@example.com" });
// Read from replica
const users = await readReplica.from(User).find({ where: { isActive: true } });
Pattern Comparison
| Feature | Query Builder | Model Manager | Model Embedding |
|---|---|---|---|
| API Simplicity | ✅ Simple | ⚠️ Verbose | ✅ Cleanest |
| Type Safety | ✅ Full | ✅ Full | ✅ Full |
| Dependency Injection | ⚠️ Requires passing sql | ✅ Perfect | ✅ Perfect |
| Testing | ⚠️ Mock the data source | ✅ Easy | ✅ Easy |
| Multiple Connections | ✅ Full | ✅ Full | ✅ Full |
When to Use Each Pattern
| Pattern | Best For |
|---|---|
| Query Builder | Most apps, quick prototypes, explicit data source usage |
| Model Manager | Enterprise apps, dependency injection, unit testing |
| Model Embedding | Service architecture, microservices, cleanest syntax |
Migrating Between Patterns
All patterns use the same underlying ModelManager, so you can switch as your application grows:
// Start with Query Builder
const user = await sql.from(User).insert({ name: "John" });
// Move to Model Manager for DI
const userManager = sql.getModelManager(User);
const user2 = await userManager.insert({ name: "John" });
// Or use Model Embedding for the cleanest syntax
const sqlWithModels = new SqlDataSource({
type: "sqlite",
database: "app.db",
models: { user: User },
});
await sqlWithModels.connect();
const user3 = await sqlWithModels.user.insert({ name: "John" });
Next: Defining Models