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 < 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.

Class Instances: All query methods (find, all, where, create, etc.) return proper class instances. For example, User.find(id) returns a User instance, not a raw hash. This enables instance methods like .save(), .update(), and .delete().

CREATE Creating Records

let result = User.create({
    "email": "[email protected]",
    "name": "Alice",
    "age": 30
})
# Returns: { "valid": true, "record":  }
# Or: { "valid": false, "errors": [...] }

let user = result["record"]
user.name  # => "Alice"
user._key  # => "abc123" (auto-generated)

READ Finding Records

# Find by ID - returns a User instance
let user = User.find("user123")
user.name  # => "Alice"

# Find all - returns array of User instances
let users = User.all
users.first.name  # => "Alice"

# Find with filter - returns array of User instances
let adults = User.where("age >= @age", { "age": 18 }).all

# Complex conditions
let results = User.where("age >= @min AND role == @role", {
    "min": 21,
    "role": "admin"
}).all

UPDATE Updating Records

# Static method: update by ID
User.update("user123", { "name": "Alice Smith", "age": 31 })

# Instance method: modify fields and save
let user = User.find("user123")
user.name = "Alice Smith"
user.save()

# Or use .update() to push current fields to DB
user.name = "Alice Smith"
user.update()

DELETE Deleting Records

# Static method
User.delete("user123")

# Instance method
let user = User.find("user123")
user.delete()

COUNT Counting Records

let total = User.count

Instance Methods

All model instances have built-in methods for persistence:

Method Description
instance.save() Insert (if new) or update (if existing). Returns the instance with _key populated.
instance.update() Persist current non-_ fields to DB. Requires _key.
instance.delete() Delete the document from DB. Requires _key. Supports soft delete.
instance.reload() Refresh instance from database.
instance.increment(field) Atomically increment a numeric field by 1 (or specified amount).
instance.decrement(field) Atomically decrement a numeric field by 1 (or specified amount).
instance.touch() Update _updated_at timestamp without changing other fields.
instance.restore() Restore a soft-deleted record (clear deleted_at).
instance.errors Return validation errors from last save/update.
# Create a new instance and save it
let user = User.new()
user.name = "Bob"
user.email = "[email protected]"
user.save()
# user._key is now set

# Modify and save again
user.name = "Robert"
user.save()

# Delete
user.delete()

Read-only fields: Fields starting with _ (_key, _id, _rev, etc.) are read-only on model instances. They are set automatically by the database and cannot be assigned directly.

Static Methods Reference

Method Description
Model.create(data) Insert a new document, returns { "valid": bool, "record": instance }
Model.create_many([data, ...]) Batch insert multiple documents, returns { "created": n }
Model.find(id) Get document by ID, returns instance or null
Model.find_by(field, value) Find first record by field value
Model.first_by(field, value) Find first record by field with ordering
Model.find_or_create_by(field, value, data?) Find by field, or create if not found
Model.where(filter, bind_vars) Query with SDBQL filter
Model.order(field, direction) Start a query chain sorted by field ("asc"/"desc")
Model.limit(n) Start a query chain limited to n results
Model.offset(n) Start a query chain with offset
Model.all Get all documents as instances
Model.count Count all documents
Model.update(id, data) Update a document
Model.upsert(id, data) Insert or update document by ID
Model.delete(id) Delete a document
Model.transaction() Get transaction handle for manual control
Model.scope(name) Execute a named scope (returns QueryBuilder)
Model.with_deleted Include soft-deleted records (QueryBuilder)
Model.only_deleted Query only deleted records (QueryBuilder)
Model.includes(rel, ...) Eager load relations (returns QueryBuilder)
Model.select(field, ...) Select specific fields (returns QueryBuilder)
Model.join(rel, filter?, binds?) Filter by related existence (returns QueryBuilder)
Model.sum(field) / avg / min / max Aggregation (returns QueryBuilder, chain .first to execute)
Model.group_by(field, func, agg_field) Group-by aggregation (returns QueryBuilder, chain .all)
Model.pluck(field, ...) Select fields only (returns QueryBuilder, chain .all)

Complete Example

# app/models/user.sl
class User < Model
    has_many("posts")
    has_one("profile")

    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 is_adult -> Bool
        this.age >= 18
    end
end

# app/models/post.sl
class Post < Model
    belongs_to("user")
    has_many("comments")

    validates("title", { "presence": true, "min_length": 3 })
end

# Usage in controller
class UsersController < Controller
    def index(req: Any)
        # Eager load posts and profiles to avoid N+1 queries
        let users = User.includes("posts", "profile").all
        render("users/index", { "users": users })
    end

    def show(req: Any)
        let id = req["params"]["id"]
        let user = User.includes("posts").find(id)
        render("users/show", { "user": user })
    end

    def active(req: Any)
        # Find active users who have at least one post
        let users = User.join("posts")
            .where("active = @a", { "a": true })
            .order("created_at", "desc")
            .limit(10)
            .all
        render("users/active", { "users": users })
    end

    def create(req: Any)
        let result = User.create({
            "name": req["params"]["name"],
            "email": req["params"]["email"]
        })
        if result["valid"]
            let user = result["record"]
            return redirect("/users/" + user._key)
        else
            render("users/new", { "errors": result["errors"] })
        end
    end

    def update(req: Any)
        let user = User.find(req["params"]["id"])
        user.name = req["params"]["name"]
        user.save()
        redirect("/users/" + user._key)
    end

    def destroy(req: Any)
        let user = User.find(req["params"]["id"])
        user.delete()
        redirect("/users")
    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
  • Declare relationships - Use has_many, has_one, belongs_to for associations
  • Use includes for eager loading - Avoid N+1 queries when accessing related data
  • Use join for filtering - When you only need to filter by existence, not preload

Explore Further