Repositories

Separate your data access logic from your controllers with the Repository pattern. Extend BaseRepository<T> to get all common CRUD operations for free — no boilerplate required.

Without BaseRepository vs with BaseRepository

Everything inside the without block is already provided by BaseRepository — you don't need to write it.

Without — lots of boilerplate
@Repository
public class ArticleRepository {

    public Article findById(int id) {
        return Model.find(Article.class, id);  // already in BaseRepository
    }

    public List<Article> findAll() {
        return Model.all(Article.class);       // already in BaseRepository
    }

    public boolean save(Article article) {
        return article.save();                 // already in BaseRepository
    }

    public boolean delete(int id) {
        Article a = Model.find(Article.class, id);
        if (a == null) return false;
        return a.delete();                     // already in BaseRepository
    }
}
With — only what's specific to your domain
@Repository
public class ArticleRepository extends BaseRepository<Article> {

    public ArticleRepository() {
        super(Article.class);
    }

    // findById, findAll, save, delete, count, paginate... all inherited

    // Only add what's specific to articles:
    public List<Article> findPublished() {
        return query().where("status", 1).orderByDesc("created_at").get();
    }

    public Optional<Article> findBySlug(String slug) {
        return Optional.ofNullable(findBy("slug", slug));
    }
}
Built-in methods
Method Description
findAll()All records
findById(id)By primary key, or null
findByIdOrFail(id)By primary key, throws ModelNotFoundException
findBy(column, value)First match on a column
findAllBy(column, value)All matches on a column
findMany(ids)Multiple records by primary key list
findByAttributes(map)First match on multiple columns (AND)
create(attributes)Insert and return new record
firstOrCreate(search, extra)Find or create
updateOrCreate(search, attributes)Find and update, or create if not found
update(id, attributes)Update by primary key
updateWhere(column, value, attributes)Bulk update by condition
delete(id)Delete by primary key
destroy(ids...)Delete multiple by primary key
deleteWhere(column, value)Bulk delete by condition
count()Total row count
countWhere(column, value)Count matching a condition
exists(id)Check existence by primary key
existsWhere(column, value)Check existence by condition
paginate(page, perPage)Paginated results (default 15 per page)
latest()All records ordered by created_at DESC
latest(limit)Last N records
oldest()All records ordered by created_at ASC
first()First record
pluck(column)Extract a single column as a list
pluckWhere(column, whereCol, whereVal)Pluck with a condition
max(column)Maximum value of a column
min(column)Minimum value of a column
sum(column)Sum of a column
avg(column)Average of a column
chunk(size, callback)Iterate all records in batches without loading the full table
chunkWhere(column, value, size, callback)Same as chunk with a WHERE filter
upsert(rows, uniqueKeys)Insert or update on conflict — updates all non-unique columns
upsert(rows, uniqueKeys, updateKeys)Insert or update on conflict — updates only specified columns
Custom queries with query()

Use query() to start a builder scoped to your model for anything that goes beyond the built-ins:

@Repository
public class ArticleRepository extends BaseRepository<Article> {

    public ArticleRepository() { super(Article.class); }

    public Paginator<Article> paginatePublished(int page) {
        return query()
                .where("status", 1)
                .orderByDesc("created_at")
                .paginate(page, 15);
    }

    public List<Article> findByAuthorWithComments(int userId) {
        return query()
                .where("user_id", userId)
                .with("comments", "author")
                .get();
    }

    public List<Article> findRecentByCategory(int categoryId, int limit) {
        return query()
                .where("category_id", categoryId)
                .where("status", 1)
                .orderByDesc("created_at")
                .limit(limit)
                .get();
    }
}
updateOrCreate

Find the first record matching search and update it with attributes, or create a new one merging both if none is found.

// Updates Alice's role if found, creates her if not
User user = userRepo.updateOrCreate(
    Map.of("email", "alice@example.com"),
    Map.of("role", "admin", "active", 1)
);
chunk

Process large tables without loading everything into memory. The callback receives one batch at a time.

// Process all users 500 at a time
userRepo.chunk(500, users -> {
    users.forEach(u -> u.set("active", 1).save());
});

// Same with a WHERE filter
userRepo.chunkWhere("role", "user", 200, users -> {
    users.forEach(u -> notificationService.send(u));
});

Uses offset-based pagination internally. Avoid inserting or deleting rows in the same table during iteration — pages may shift.

upsert

Insert multiple rows, updating on conflict. Uses dialect-specific syntax — ON DUPLICATE KEY UPDATE on MySQL, ON CONFLICT DO UPDATE on PostgreSQL and SQLite.

List<Map<String, Object>> rows = List.of(
    Map.of("email", "alice@example.com", "name", "Alice", "role", "admin"),
    Map.of("email", "bob@example.com",   "name", "Bob",   "role", "user")
);

// Conflict on email — updates all non-unique columns on conflict
userRepo.upsert(rows, List.of("email"));

// Only update the role column on conflict
userRepo.upsert(rows, List.of("email"), List.of("role"));

The uniqueKeys column must have a UNIQUE or PRIMARY KEY constraint in the database schema. SQLite requires version 3.24+.

Injection in a controller
@Controller("/articles")
public class ArticleController extends BaseController {

    @GET("")
    private Object index(ArticleRepository articles, Request req) {
        int page = Integer.parseInt(req.queryParamOrDefault("page", "1"));
        return render("articles/index.html", Map.of(
            "articles", articles.paginatePublished(page)
        ));
    }

    @GET("/:id")
    private Object show(ArticleRepository articles, Request req) {
        Article article = articles.findByIdOrFail(req.params("id"));
        return render("articles/show.html", Map.of("article", article));
    }
}
💡 Best practice

Always extend BaseRepository. Only add methods that are specific to your domain — everything else is already there. Any class annotated with @Repository is automatically available for injection via @Inject or as a controller method parameter.