Repositories

Table of contents

  1. Overview
  2. Store interfaces
    1. DataStore
    2. JdbcStore
  3. TableRegistry
  4. Creating a ModelRepository
    1. Flat-map store (no SQL)
    2. SQL store
    3. Custom SQL dialect
  5. CRUD operations
    1. save(model) — upsert
    2. find(id) — read by primary key
    3. exists(id)
    4. delete(id)
  6. Querying
  7. Bulk and conditional delete
    1. deleteWhere(column, value)
    2. deleteWhere(Query)
    3. deleteAll(List<String> ids) — bulk delete by primary key
    4. deleteWhereInSubquery(column, subquery) — SQL only
    5. deleteWhereExists(subquery) — SQL only
  8. Test isolation
  9. AbstractRepository

Overview

ModelRepository<T> is the single entry point for all persistence operations. It inspects the store it receives at construction time and routes every call to either the SQL path (JDBC, via JdbcStore) or the flat-map path (DataStore).

ModelRepository<T>
  ├── JdbcStore path  → used when store implements JdbcStore AND a TableRegistry entry exists
  └── DataStore path  → fallback for any store implementing only DataStore

Store interfaces

DataStore

The minimal persistence contract — key/value pairs keyed by a string path:

public interface DataStore {
    void                       save(String path, Map<String, Object> data);
    Optional<Map<String, Object>> load(String path);
    void                       delete(String path);
    boolean                    exists(String path);
}

JdbcStore

Extend DataStore with parameterized SQL execution:

public interface JdbcStore extends DataStore {
    List<Map<String, Object>> query(String sql, List<Object> params);
    int executeUpdate(String sql, List<Object> params);
}

Always pass user-supplied values through List<Object> params (the ? bind parameters). The SQL path never interpolates model attribute values directly into query strings, making SQL injection structurally impossible.

To activate the SQL path, implement both DataStore and JdbcStore on your store class:

public class MyDataSource implements DataStore, JdbcStore {

    @Override
    public List<Map<String, Object>> query(String sql, List<Object> params) {
        // execute with your JDBC connection pool
    }

    @Override
    public int executeUpdate(String sql, List<Object> params) {
        // execute INSERT / UPDATE / DELETE
    }

    // ... DataStore methods ...
}

TableRegistry

TableRegistry maps a repository prefix to a SQL table name and its column definitions. A JdbcStore entry alone is not enough — a matching registry entry must also exist for the SQL path to activate.

TableRegistry.register(
    "players",          // prefix — matches the repo constructor argument
    "player_data",      // SQL table name
    Map.of(
        "id",      "VARCHAR(36) PRIMARY KEY",
        "name",    "VARCHAR(64)",
        "coins",   "INT",
        "level",   "INT"
    )
);
Method Description
TableRegistry.register(prefix, tableName, columns) Register or overwrite a table entry
TableRegistry.get(prefix) Returns TableMeta or null
TableRegistry.all() Unmodifiable view of all entries

TableRegistry is JVM-static. In tests, use a unique UUID.randomUUID().toString() prefix per test class to prevent cross-test pollution. See the testing guide.


Creating a ModelRepository

Flat-map store (no SQL)

ModelRepository<Player> repo = new ModelRepository<>(
    myDataStore,    // implements DataStore
    "players",      // prefix used for path construction and TableRegistry lookup
    (id, data) -> {
        Player p = new Player(id);
        p.fromMap(data);
        return p;
    }
);

SQL store

// 1. Register the table
TableRegistry.register("players", "player_data", columnMap);

// 2. Provide a store that implements both DataStore and JdbcStore
ModelRepository<Player> repo = new ModelRepository<>(
    myJdbcDataStore,
    "players",
    (id, data) -> { Player p = new Player(id); p.fromMap(data); return p; }
);
// → all repo calls now use SQL automatically

Custom SQL dialect

Pass an explicit SqlDialect as the fourth argument:

import com.github.ezframework.javaquerybuilder.query.dialect.SqlDialect;

ModelRepository<Player> repo = new ModelRepository<>(
    store, "players", factory, SqlDialect.MYSQL
);

Supported dialects: STANDARD, MYSQL, POSTGRESQL, H2.


CRUD operations

save(model) — upsert

On the SQL path this executes an INSERT … ON DUPLICATE KEY UPDATE equivalent (dialect-aware). On the flat-map path it calls DataStore.save.

repo.save(player); // returns void
player.save(repo); // convenience shortcut on Model — returns this

find(id) — read by primary key

Optional<Player> result = repo.find("some-uuid");
result.ifPresent(p -> System.out.println(p.getName()));

exists(id)

boolean active = repo.exists("some-uuid");

delete(id)

repo.delete("some-uuid");
player.delete(repo); // convenience shortcut on Model

Querying

Pass a Query object built by Model.queryBuilder() to repo.query():

List<Player> rich = repo.query(
    Model.queryBuilder()
        .where("coins", ">", 1000)
        .orderBy("name", true)
        .limit(20)
        .build()
);

See the Queries page for the full query builder reference.


Bulk and conditional delete

deleteWhere(column, value)

repo.deleteWhere("status", "banned");

deleteWhere(Query)

repo.deleteWhere(
    Model.queryBuilder().where("coins", "<", 0).build()
);

deleteAll(List<String> ids) — bulk delete by primary key

On the SQL path this generates a single DELETE … WHERE id IN (?, ?, …).

repo.deleteAll(List.of("uuid-1", "uuid-2", "uuid-3"));

deleteWhereInSubquery(column, subquery) — SQL only

Query activePlayers = Model.queryBuilder()
    .from("player_data")
    .where("status", "=", "active")
    .build();

repo.deleteWhereInSubquery("player_id", activePlayers);
// → DELETE FROM ... WHERE player_id IN (SELECT ...)

deleteWhereExists(subquery) — SQL only

repo.deleteWhereExists(subquery);
// → DELETE FROM ... WHERE EXISTS (SELECT ...)

Test isolation

When writing integration tests against a JVM-static TableRegistry, isolate each test class with a unique prefix so entries never collide:

private final String prefix = "test-" + UUID.randomUUID();

@BeforeEach
void setUp() {
    TableRegistry.register(prefix, "player_data", Map.of(
        "id",     "VARCHAR(36) PRIMARY KEY",
        "name",   "VARCHAR(64)"
    ));
    repo = new ModelRepository<>(store, prefix, factory);
}

AbstractRepository

For non-Model domain objects, extend AbstractRepository<T, ID> which provides find, findAll, save, and delete backed by DataStore with built-in SLF4J logging and Micrometer counter instrumentation.

public class PlayerRepository extends AbstractRepository<Player, String> {

    public PlayerRepository(DataStore store) {
        super(store, "players");
    }

    @Override
    protected Map<String, Object> toMap(Player p) { return p.toMap(); }

    @Override
    protected Player fromMap(Map<String, Object> data) {
        Player p = new Player((String) data.get("id"));
        p.fromMap(data);
        return p;
    }

    @Override
    protected String extractId(Player p) { return p.getId(); }
}