Models & ORM
Models manage data and business logic in your MVC application. SoliLang provides a simple OOP-style interface for database operations.
Defining Models
Create model files in app/models/. The collection name is automatically derived from the class name:
User→"users"BlogPost→"blog_posts"UserProfile→"user_profiles"
class User extends Model { }
That's it! No need to manually specify collection names or field definitions.
CRUD Operations
CREATE Creating Records
let result = User.create({
"email": "[email protected]",
"name": "Alice",
"age": 30
});
// Returns: { "valid": true, "record": { "id": "...", ... } }
// Or: { "valid": false, "errors": [...] }
READ Finding Records
// Find by ID
let user = User.find("user123");
// Find all
let users = User.all();
// Find with SDBQL filter
// Note: where() returns a QueryBuilder - call .all() to get results
let adults = User.where("doc.age >= @age", { "age": 18 }).all();
// Complex conditions
let results = User.where("doc.age >= @min AND doc.role == @role", {
"min": 21,
"role": "admin"
}).all();
UPDATE Updating Records
User.update("user123", {
"name": "Alice Smith",
"age": 31
});
DELETE Deleting Records
User.delete("user123");
COUNT Counting Records
let total = User.count();
Query Builder Chaining
Chain methods to build complex queries:
let results = User
.where("doc.age >= @age", { "age": 18 })
.where("doc.active == @active", { "active": true })
.order("created_at", "desc")
.limit(10)
.offset(20)
.all();
// Get first result only
let first = User.where("doc.email == @email", { "email": "[email protected]" }).first();
// Count with conditions
let count = User.where("doc.role == @role", { "role": "admin" }).count();
Static Methods Reference
| Method | Description |
|---|---|
Model.create(data) |
Insert a new document |
Model.find(id) |
Get document by ID |
Model.where(filter, bind_vars) |
Query with SDBQL filter |
Model.all() |
Get all documents |
Model.update(id, data) |
Update a document |
Model.delete(id) |
Delete a document |
Model.count() |
Count all documents |
QueryBuilder Methods
| Method | Description |
|---|---|
.where(filter, bind_vars) |
Add filter condition (ANDed) |
.order(field, direction) |
Set sort order ("asc"/"desc") |
.limit(n) |
Limit results to n documents |
.offset(n) |
Skip first n documents |
.all() |
Execute, return all results |
.first() |
Execute, return first result |
.count() |
Execute, return count |
Validations
Define validation rules in your model class:
class User extends Model {
validates("email", { "presence": true, "uniqueness": true })
validates("name", { "presence": true, "min_length": 2, "max_length": 100 })
validates("age", { "numericality": true, "min": 0, "max": 150 })
validates("website", { "format": "^https?://" })
}
Validation Options
| Option | Description |
|---|---|
presence: true |
Field must be present and not empty |
uniqueness: true |
Value must be unique in collection |
min_length: n |
String must be at least n characters |
max_length: n |
String must be at most n characters |
format: "regex" |
String must match regex pattern |
numericality: true |
Value must be a number |
min: n |
Number must be >= n |
max: n |
Number must be <= n |
custom: "method" |
Call custom validation method |
Validation Results
let result = User.create({ "email": "" });
if result["valid"] {
let user = result["record"];
print("Created user: " + user["id"]);
} else {
for error in result["errors"] {
print(error["field"] + ": " + error["message"]);
}
}
Callbacks
Define lifecycle callbacks to run code at specific points:
class User extends Model {
before_save("normalize_email")
after_create("send_welcome_email")
before_update("log_changes")
after_delete("cleanup_related")
fn normalize_email() -> Any {
this.email = this.email.downcase();
}
fn send_welcome_email() -> Any {
// Send email logic
}
}
Available Callbacks
before_save
Before create or update
after_save
After create or update
before_create
Before inserting new record
after_create
After inserting new record
before_update
Before updating record
after_update
After updating record
before_delete
Before deleting record
after_delete
After deleting record
Relationships
Implement relationships using model methods:
class Post extends Model {
fn author() -> Any {
return User.find(this.author_id);
}
}
class User extends Model {
// Returns a QueryBuilder for chaining
fn posts() -> Any {
return Post.where("doc.author_id == @id", { "id": this.id });
}
}
// Usage
let post = Post.find("post123");
let author = post.author();
let user = User.find("user123");
// posts() returns QueryBuilder - chain .all() to get results
let user_posts = user.posts().all();
// Or chain more methods before executing
let recent_posts = user.posts().order("created_at", "desc").limit(5).all();
Custom Methods
Add custom methods to your models:
class User extends Model {
fn is_admin() -> Bool {
return this.role == "admin";
}
fn full_name() -> String {
return this.first_name + " " + this.last_name;
}
}
// Usage
let user = User.find("user123");
if user.is_admin() {
print("Welcome, admin " + user.full_name());
}
Query Generation (SDBQL)
Under the hood, Model methods generate SDBQL (SoliDB Query Language) queries:
| Method | Generated SDBQL |
|---|---|
User.all() |
FOR doc IN users RETURN doc |
User.where("doc.age >= @age", {"age": 18}) |
FOR doc IN users FILTER doc.age >= @age RETURN doc |
.order("name", "asc") |
... SORT doc.name ASC RETURN doc |
.limit(10).offset(20) |
... LIMIT 20, 10 RETURN doc |
User.count() |
FOR doc IN users COLLECT WITH COUNT INTO count RETURN count |
SDBQL Syntax
FOR doc IN collectioninstead ofSELECT * FROMFILTER expressioninstead ofWHERESORT doc.field ASC/DESCinstead ofORDER BY@variablesyntax for bind parameters
Complete Example
// app/models/user.soli
class User extends Model {
validates("email", { "presence": true, "uniqueness": true })
validates("name", { "presence": true, "min_length": 2 })
before_save("normalize_email")
fn normalize_email() -> Any {
this.email = this.email.downcase();
}
fn posts() -> Any {
return Post.where("doc.user_id == @id", { "id": this.id });
}
fn is_adult() -> Bool {
return this.age >= 18;
}
}
// Usage in controller
class UsersController extends Controller {
fn index(req: Any) -> Any {
let users = User.all();
return render("users/index", { "users": users });
}
fn show(req: Any) -> Any {
let id = req["params"]["id"];
let user = User.find(id);
let posts = user.posts().order("created_at", "desc").limit(5).all();
return render("users/show", { "user": user, "posts": posts });
}
fn create(req: Any) -> Any {
let result = User.create({
"name": req["params"]["name"],
"email": req["params"]["email"]
});
if result["valid"] {
return redirect("/users/" + result["record"]["id"]);
} else {
return render("users/new", { "errors": result["errors"] });
}
}
}
Best Practices
- Keep models simple - Just extend
Model, no configuration needed - Use meaningful class names - They become collection names automatically
- Add validations - Validate data before it reaches the database
- Use callbacks wisely - Keep them focused and avoid heavy operations
- Add custom methods - Encapsulate business logic in model methods
- Use relationships - Create methods that return related models