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.
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.
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"); }
}
// 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();
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();
@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();
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();
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.
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();
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();
}
}