DEV Community

カイノヴァイイ
カイノヴァイイ

Posted on

Why I Replaced ActiveJDBC With My Own ORM

Why I Replaced ActiveJDBC With My Own ORM

I'm building Obsidian, a Java web framework. The database layer used to run on ActiveJDBC. Then I ripped it out and wrote my own.

Not because ActiveJDBC is bad. But because a framework that depends on another framework isn't really in control of itself.


The Problem With ActiveJDBC

ActiveJDBC lets you write User.where("active = ?", 1) — but Java has a fundamental issue: an inherited static method doesn't know which subclass it's being called from. Model.where() can't figure out on its own that it should query the users table.

To work around this, ActiveJDBC rewrites your .class files after compilation — injecting static methods directly into each subclass. In practice: a mandatory Maven/Gradle plugin running in the process-classes phase, a build step that can silently break when your toolchain changes, and an onboarding experience where the first question is always "why isn't my model working?".

For a personal project, that's fine. For a distributed framework, it's a dealbreaker.

The other reason: dependencies as liabilities. My long-term goal is for Obsidian to run on Spark Java and nothing else. Every library removed is one less thing that can go wrong.


What It Looks Like Now

An Obsidian model is a class that describes itself — no bytecode manipulation, no plugin, just methods:

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

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

    public BelongsTo<StaffRank> staffRank() {
        return belongsTo(StaffRank.class, "StaffRankID");
    }

    public HasOne<FactionPlayer> factionPlayer() {
        return hasOne(FactionPlayer.class, "UUID");
    }
}
Enter fullscreen mode Exit fullscreen mode

Table resolution happens through a metadata cache initialized on first access — @Table annotation or derived from the class name. Zero build-time magic.

The Repository Pattern

The model alone isn't enough — you don't want query logic scattered all over the codebase. Obsidian pushes a Repository pattern: a class annotated with @Repository that extends BaseRepository<T> and centralizes all the query logic for a given model.

@Repository
public class UserRepository extends BaseRepository<User>
{
    public UserRepository() {
        super(User.class);
    }

    public User findByUUID(String uuid) {
        return query()
                .with("staffRank", "vipRank")
                .where("UUID", uuid)
                .first();
    }

    public List<User> findByStaffRank(int staffRankId) {
        return query()
                .with("staffRank", "vipRank")
                .where("StaffRankID", staffRankId)
                .get();
    }

    public boolean userExists(String pseudo) {
        return existsWhere("Pseudo", pseudo);
    }
}
Enter fullscreen mode Exit fullscreen mode

@Repository gets picked up at startup by the component scanner and registered in the DI container. Any controller or service can then receive it via @Inject — no manual instantiation.

With ActiveJDBC, queries lived anywhere. Now they have a home:

// Before — ActiveJDBC, scattered across the codebase
List<User> users = User.where("active = ?", 1).orderBy("name").limit(10).load();

// After — Obsidian, centralized in the repository
List<User> users = userRepository.findAll();
Enter fullscreen mode Exit fullscreen mode

Migrations stay fluent:

public class CreateUsersTable extends Migration {
    @Override
    public void up() {
        createTable("users", t -> {
            t.id();
            t.string("name").notNull();
            t.string("email").unique().notNull();
            t.boolean_("is_admin").defaultValue(false);
            t.timestamps();
            t.softDeletes();
        });
    }

    @Override
    public void down() {
        dropTable("users");
    }
}
Enter fullscreen mode Exit fullscreen mode

Blueprint adapts its output for MySQL, PostgreSQL and SQLite through a Grammar abstraction — no hardcoded dialect in the migrations.


Conclusion

The instrumentation plugin is gone from the build. The ActiveJDBC dependency — and everything it pulled in — gone.

The ORM I have now is entirely mine. When something breaks, I read my own code. When I need a feature, I add it.

The lesson isn't "never use third-party libraries." It's that some dependencies sit at the core of what you're building — and at that depth, owning the code is worth the investment.

Obsidian is open source at github.com/obsidian-framework.

Top comments (0)