Models

Models use Obsidian's native ORM, which follows the ActiveRecord pattern. A model maps directly to a database table — no configuration needed beyond the class name.

Create a model
import com.obsidian.core.database.orm.model.Model;
import com.obsidian.core.database.orm.model.Table;

@Table("articles")
public class Article extends Model {

    public String getTitle()   { return getString("title"); }
    public String getContent() { return getString("content"); }
    public Integer getStatus() { return getInteger("status"); }

    public void setTitle(String title)     { set("title", title); }
    public void setContent(String content) { set("content", content); }
    public void setStatus(Integer status)  { set("status", status); }
}

By convention, Article maps to the articles table automatically. Use @Table to override when your table name doesn't follow the convention.

Custom primary key

If your table uses a column other than id as primary key, override primaryKey():

@Table("game_users")
public class User extends Model {

    @Override
    public String primaryKey() { return "UUID"; }

    public String getUUID() { return getString("UUID"); }
}
CRUD operations
// Create
Article article = new Article();
article.setTitle("My title");
article.setContent("My content");
article.save();

// Retrieve by primary key
Article article = Model.find(Article.class, 1);

// Retrieve all
List<Article> all = Model.all(Article.class);

// Query with conditions
List<Article> published = Model.query(Article.class)
        .where("status", 1)
        .orderBy("created_at", "DESC")
        .get();

// First match
Article latest = Model.query(Article.class)
        .orderByDesc("created_at")
        .first();

// Update
article.setTitle("New title");
article.save();

// Delete
article.delete();
QueryBuilder

The fluent QueryBuilder supports the most common SQL patterns:

// WHERE conditions
Model.query(Article.class)
        .where("status", 1)
        .where("user_id", ">", 10)
        .whereIn("category_id", List.of(1, 2, 3))
        .whereBetween("views", 100, 1000)
        .whereLike("title", "%java%")
        .get();

// ORDER, LIMIT, OFFSET
Model.query(Article.class)
        .orderBy("created_at", "DESC")
        .limit(10)
        .offset(20)
        .get();

// Pagination
Paginator<Article> page = Model.query(Article.class)
        .orderByDesc("created_at")
        .paginate(1, 15);  // page 1, 15 per page

// Aggregates
long count = Model.query(Article.class).where("status", 1).count();
Object max  = Model.query(Article.class).max("views");

// Check existence
boolean exists = Model.query(Article.class).where("slug", slug).exists();
Relations
@Table("articles")
public class Article extends Model {

    // BelongsTo
    public BelongsTo<User> author() {
        return belongsTo(User.class, "user_id");
    }

    // HasMany
    public HasMany<Comment> comments() {
        return hasMany(Comment.class, "article_id");
    }

    // HasOne
    public HasOne<Thumbnail> thumbnail() {
        return hasOne(Thumbnail.class, "article_id");
    }
}

// Access
User author            = article.author().first();
List<Comment> comments = article.comments().get();

// Eager loading — avoids N+1
List<Article> articles = Model.query(Article.class)
        .with("author", "comments")
        .get();
Soft deletes

Enable soft deletes by overriding softDeletes(). Deleted records are filtered out automatically — they are not permanently removed.

@Table("articles")
public class Article extends Model {

    @Override
    protected boolean softDeletes() { return true; }
}

article.delete();          // sets deleted_at, keeps the row
article.restore();         // clears deleted_at
article.forceDelete();     // permanently removes the row

// Include soft-deleted records
Model.query(Article.class).withTrashed().get();
Model.query(Article.class).onlyTrashed().get();
Cache

Add @Cacheable to cache query results automatically. The cache is invalidated on every save() and delete().

@Cacheable(ttl = 300)   // 5 minutes — configurable per model
@Table("staff_ranks")
public class StaffRank extends Model { ... }

// Manual flush when needed
ModelCache.flush(StaffRank.class);

Requires a cache driver to be configured. See the Cache section.

Mass assignment

Control which attributes can be mass-assigned via fill():

@Table("articles")
public class Article extends Model {

    @Override
    protected List<String> fillable() {
        return List.of("title", "content", "status");
    }
}

// Create from a map (e.g. request body)
Article article = new Article();
article.fill(Map.of("title", "Hello", "content", "World"));
article.save();
Observers

Hook into the model lifecycle with an observer:

public class ArticleObserver extends ModelObserver<Article> {

    @Override
    public boolean creating(Article article) {
        article.set("slug", slugify(article.getTitle()));
        return true;  // return false to cancel the operation
    }

    @Override
    public void created(Article article) {
        // notify, index, etc.
    }
}

// Register in the model
@Table("articles")
public class Article extends Model {

    @Override
    protected ModelObserver<Article> observer() {
        return new ArticleObserver();
    }
}