Skip to content

Obsidian ORM Replace ActiveJDBC#60

Merged
kainovaii merged 5 commits intomainfrom
dev
Mar 20, 2026
Merged

Obsidian ORM Replace ActiveJDBC#60
kainovaii merged 5 commits intomainfrom
dev

Conversation

@kainovaii
Copy link
Member

Obsidian ORM Replace ActiveJDBC

Overview

This PR replaces ActiveJDBC with Obsidian's native ORM. No more bytecode instrumentation, no more Maven plugin to configure — the ORM integrates directly into the framework lifecycle and supports MySQL, PostgreSQL and SQLite.


Changelog v3.0.0

New — Native ORM

  • Fluent QueryBuilder with native SQL injection protection (identifier + operator whitelist)
  • Full relation support: HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphOne, MorphMany, MorphTo
  • Eager loading on all relations via .with() — eliminates N+1
  • Soft deletes, automatic timestamps, observers, global scopes
  • Fluent migrations with Blueprint (MySQL, PostgreSQL, SQLite)
  • Native pagination with Paginator
  • Result streaming with chunk()
  • Query cache via @Cacheable — InMemory or Redis

Security & stability

  • Connection leak fixedfinally in RouteHandler guarantees connection close even on exception
  • Error messages — SQL logged internally only, generic message exposed externally
  • insertAll() wrapped in transaction — automatic rollback on partial failure
  • whereIn([]) — empty list generates 1 = 0 instead of crashing with invalid SQL
  • whereNotIn([]) — empty list is a no-op
  • forPage(0) — throws IllegalArgumentException instead of generating a negative offset
  • update({}) — clean short-circuit, returns 0 without touching the DB
  • requireWhere() — optional guard against full-table update/delete

Performance

  • BelongsToMany.eagerLoad() — 1 JOIN instead of 2 queries
  • resultSetToList() — initial capacity on ArrayList and LinkedHashMap
  • Model metadata cached per class (static ConcurrentHashMap)
  • @Cacheable(ttl = 300) — PK and query cache, auto-invalidation on save()/delete()

Migration Guide — ActiveJDBC → Obsidian ORM

1. Dependencies

Remove from pom.xml:

<dependency>
    <groupId>org.javalite</groupId>
    <artifactId>activejdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.javalite</groupId>
    <artifactId>activejdbc-instrumentation</artifactId>
</dependency>

Also remove the instrumentation plugin from the build if present. The obsidian-core dependency now covers the ORM.


2. Models

// BEFORE
import org.javalite.activejdbc.Model;
import org.javalite.activejdbc.annotations.Table;
import org.javalite.activejdbc.annotations.IdName;
 
@Table("game_users")
@IdName("UUID")
public class User extends Model {}
// AFTER
import com.obsidian.core.database.orm.model.Model;
import com.obsidian.core.database.orm.model.Table;
 
@Table("game_users")
public class User extends Model {
    @Override
    public String primaryKey() { return "UUID"; }
}

@IdName is gone — the primary key is declared via primaryKey().
getString(), getInteger(), getLong(), getBoolean(), set() are identical.


3. Querying

// BEFORE
User.findById(id)
User.findFirst("UUID = ?", uuid)
User.findAll().load()
User.where("VipRankID = ?", id).load()
User.where("UUID IN (?)", uuids).load()
Faction.where("1=1").orderBy("name ASC").load()
// Multi-param
FactionChunk.findFirst("x = ? AND z = ?", x, z)
 
// AFTER
Model.find(User.class, id)
Model.where(User.class, "UUID", uuid).first()
Model.all(User.class)
Model.query(User.class).where("VipRankID", id).get()
Model.query(User.class).whereIn("UUID", new ArrayList<>(uuids)).get()
Model.query(Faction.class).orderBy("name", "ASC").get()
// Multi-param
Model.query(FactionChunk.class).where("x", x).where("z", z).first()

ActiveJDBC anti-pattern to fix — never put ORDER BY inside where():

// BEFORE (anti-pattern)
StaffRank.where("1=1 ORDER BY hierarchy ASC").load()
Report.where("1=1").orderBy("date_signalement ASC").load()
VipRank.findFirst("1=1 ORDER BY priority DESC")

// AFTER
Model.query(StaffRank.class).orderBy("hierarchy", "ASC").get()
Model.query(Report.class).orderBy("date_signalement", "ASC").get()
Model.query(VipRank.class).orderByDesc("priority").first()

4. Persistence

// BEFORE            // AFTER
model.saveIt()  →    model.save()  // saveIt() also accepted
model.delete()  →    model.delete()  // identical

5. Connections & transactions

ActiveJDBC requires Base.open() / Base.close(). The Obsidian ORM manages the connection lifecycle automatically via DatabaseMiddleware — nothing to do in controllers.

// BEFORE
Base.open(driver, url, user, password);
// ...
Base.close();
 
// AFTER — nothing, it's automatic

For transactions:

// BEFORE
Base.openTransaction();
try {
    // ...
    Base.commitTransaction();
} catch (Exception e) {
    Base.rollbackTransaction();
}
 
// AFTER
DB.withTransaction(() -> {
    // ...
    return null;
});

DB.withConnection() is unchanged — used in LiveComponents to explicitly open a connection outside the HTTP lifecycle:

// Unchanged — works as-is
DB.withConnection(() -> userService.findAll().stream().toList());

6. Cache (new)

For frequently read models, add @Cacheable:

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

Cache is automatically invalidated on save() and delete(). Manual flush:

ModelCache.flush(StaffRank.class);

7. Quick reference

ActiveJDBC Obsidian ORM Notes
@Table @Table Different import
@IdName("col") primaryKey() { return "col"; } Method override
extends Model extends Model Different import
getString / getInteger / getLong / getBoolean Identical No change
set(key, value) Identical No change
saveIt() save() or saveIt() Both accepted
delete() Identical No change
findById(id) Model.find(Cls.class, id)
findFirst("col = ?", v) Model.where(Cls.class, "col", v).first()
findFirst("a = ? AND b = ?", x, y) .query().where("a", x).where("b", y).first()
findAll().load() Model.all(Cls.class)
where("col = ?", v).load() .query().where("col", v).get()
where("1=1 ORDER BY col ASC") .orderBy("col", "ASC") Never put ORDER BY in where()
Base.open() / close() Automatic Handled by DatabaseMiddleware
Base.openTransaction() DB.withTransaction(() -> {})
DB.withConnection() Identical No change

Tests

Tests run: 461, Failures: 0, Errors: 0, Skipped: 0

@kainovaii kainovaii merged commit 8de7b69 into main Mar 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant