ESC
Type to search...
S
Soli Docs

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"
app/models/user.soli
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 collection instead of SELECT * FROM
  • FILTER expression instead of WHERE
  • SORT doc.field ASC/DESC instead of ORDER BY
  • @variable syntax 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

Next Steps