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"

Automatic Collection Creation: When you call a Model method (like create(), all(), find(), etc.) on a collection that doesn't exist yet, SoliLang will automatically create the collection for you. This means you can start using your models immediately without running migrations first.

class User extends Model end

That's it! No need to manually specify collection names or field definitions.

CRUD Operations

Auto-creation: All Model operations automatically create the collection if it doesn't exist. This only happens on the first call that encounters a missing collection.

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
    def is_admin -> Bool
        this.role == "admin"
    end

    def full_name -> String
        this.first_name + " " + this.last_name
    end
end

# Usage
let user = User.find("user123");
if user.is_admin()
    print("Welcome, admin " + user.full_name());
end

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"]);
    end
end

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")

    def normalize_email
        this.email = this.email.downcase();
    end

    def send_welcome_email
        # Send email logic
    end
end

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
    def author
        User.find(this.author_id)
    end
end

class User extends Model
    # Returns a QueryBuilder for chaining
    def posts
        Post.where("doc.author_id == @id", { "id": this.id })
    end
end

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.sl
class User extends Model
    validates("email", { "presence": true, "uniqueness": true })
    validates("name", { "presence": true, "min_length": 2 })

    before_save("normalize_email")

    def normalize_email
        this.email = this.email.downcase();
    end

    def posts
        Post.where("doc.user_id == @id", { "id": this.id })
    end

    def is_adult -> Bool
        this.age >= 18
    end
end

# Usage in controller
class UsersController extends Controller
    def index(req: Any)        let users = User.all();
        render("users/index", { "users": users })
    end

    def show(req: Any)        let id = req["params"]["id"];
        let user = User.find(id);
        let posts = user.posts().order("created_at", "desc").limit(5).all();
        render("users/show", { "user": user, "posts": posts })
    end

    def create(req: Any)        let result = User.create({
            "name": req["params"]["name"],
            "email": req["params"]["email"]
        });

        if result["valid"]
            return redirect("/users/" + result["record"]["id"]);
        else
            render("users/new", { "errors": result["errors"] })
        end
    end
end

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
  • Use migrations in production - Define indexes and schema for optimal performance

Next Steps