or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-patterns.mdcore-operations.mddatabase-init.mddatasource.mdembedded-database.mdexception-translation.mdindex.mdkey-generation.mdlob-support.mdresult-parameter-handling.mdsimplified-operations.mdsqlrowset.mdtransactions.md
tile.json

core-operations.mddocs/

Core JDBC Operations

Core classes for executing SQL: JdbcTemplate, NamedParameterJdbcTemplate, JdbcClient.

JdbcTemplate

Basic JDBC operations with positional (?) parameters.

API

public class JdbcTemplate {
    public JdbcTemplate(DataSource dataSource) {}

    // Query single value
    public <T> T queryForObject(String sql, Class<T> type, Object... args) {}
    public <T> T queryForObject(String sql, RowMapper<T> mapper, Object... args) {}

    // Query lists
    public <T> List<T> query(String sql, RowMapper<T> mapper, Object... args) {}
    public <T> List<T> queryForList(String sql, Class<T> type, Object... args) {}
    public List<Map<String, Object>> queryForList(String sql, Object... args) {}

    // Query streaming
    public <T> Stream<T> queryForStream(String sql, RowMapper<T> mapper, Object... args) {}

    // Updates
    public int update(String sql, Object... args) {}
    public int update(PreparedStatementCreator psc, KeyHolder keyHolder) {}

    // Batch
    public int[] batchUpdate(String sql, List<Object[]> batchArgs) {}
    public int[] batchUpdate(String sql, BatchPreparedStatementSetter pss) {}

    // Custom processing
    public <T> T execute(ConnectionCallback<T> action) {}
    public <T> T execute(String sql, PreparedStatementCallback<T> action) {}
    public <T> T query(String sql, ResultSetExtractor<T> rse, Object... args) {}
    public void query(String sql, RowCallbackHandler rch, Object... args) {}

    // Configuration
    public void setFetchSize(int fetchSize) {}
    public void setMaxRows(int maxRows) {}
    public void setQueryTimeout(int timeout) {}
}

Query Patterns

Single value:

String name = jdbc.queryForObject("SELECT name FROM users WHERE id = ?", String.class, 123);
Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM users WHERE status = ?", Integer.class, "active");

Single object:

User user = jdbc.queryForObject(
    "SELECT id, name, email FROM users WHERE id = ?",
    (rs, row) -> new User(rs.getLong("id"), rs.getString("name"), rs.getString("email")),
    123
);

List:

List<User> users = jdbc.query(
    "SELECT id, name, email FROM users WHERE status = ?",
    (rs, row) -> new User(rs.getLong("id"), rs.getString("name"), rs.getString("email")),
    "active"
);

List<String> names = jdbc.queryForList("SELECT name FROM users", String.class);

Map results:

Map<String, Object> row = jdbc.queryForMap("SELECT * FROM users WHERE id = ?", 123);
List<Map<String, Object>> rows = jdbc.queryForList("SELECT * FROM users WHERE status = ?", "active");

Streaming:

try (Stream<User> stream = jdbc.queryForStream(
    "SELECT * FROM users WHERE status = ?",
    (rs, row) -> new User(...),
    "active"
)) {
    stream.filter(u -> u.getEmail().contains("@company.com"))
          .forEach(this::process);
}

Update Patterns

Simple updates:

int rows = jdbc.update("UPDATE users SET status = ? WHERE id = ?", "inactive", 123);
int rows = jdbc.update("INSERT INTO users (name, email) VALUES (?, ?)", "John", "john@example.com");
int rows = jdbc.update("DELETE FROM users WHERE status = ?", "inactive");

With generated keys:

KeyHolder keyHolder = new GeneratedKeyHolder();
jdbc.update(con -> {
    PreparedStatement ps = con.prepareStatement(
        "INSERT INTO users (name, email) VALUES (?, ?)",
        Statement.RETURN_GENERATED_KEYS
    );
    ps.setString(1, "John");
    ps.setString(2, "john@example.com");
    return ps;
}, keyHolder);
Long id = keyHolder.getKey().longValue();

Batch:

List<Object[]> batch = List.of(
    new Object[]{"John", "john@example.com"},
    new Object[]{"Jane", "jane@example.com"}
);
int[] counts = jdbc.batchUpdate("INSERT INTO users (name, email) VALUES (?, ?)", batch);

// With BatchPreparedStatementSetter
int[] counts = jdbc.batchUpdate(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    new BatchPreparedStatementSetter() {
        public void setValues(PreparedStatement ps, int i) throws SQLException {
            ps.setString(1, names.get(i));
            ps.setString(2, emails.get(i));
        }
        public int getBatchSize() { return names.size(); }
    }
);

NamedParameterJdbcTemplate

Supports named parameters (:paramName) instead of positional ?.

API

public class NamedParameterJdbcTemplate {
    public NamedParameterJdbcTemplate(DataSource ds) {}
    public NamedParameterJdbcTemplate(JdbcTemplate template) {}

    // Query
    public <T> T queryForObject(String sql, Map<String,?> params, Class<T> type) {}
    public <T> T queryForObject(String sql, SqlParameterSource params, RowMapper<T> mapper) {}
    public <T> List<T> query(String sql, Map<String,?> params, RowMapper<T> mapper) {}
    public <T> Stream<T> queryForStream(String sql, Map<String,?> params, RowMapper<T> mapper) {}

    // Update
    public int update(String sql, Map<String,?> params) {}
    public int update(String sql, SqlParameterSource params, KeyHolder keyHolder) {}

    // Batch
    public int[] batchUpdate(String sql, Map<String,?>[] batch) {}
    public int[] batchUpdate(String sql, SqlParameterSource[] batch) {}
}

Patterns

Basic usage:

Map<String, Object> params = Map.of("status", "active", "minAge", 18);
List<User> users = named.query(
    "SELECT * FROM users WHERE status = :status AND age >= :minAge",
    params,
    (rs, row) -> new User(...)
);

IN clauses:

List<Integer> ids = List.of(1, 2, 3, 4, 5);
Map<String, Object> params = Map.of("ids", ids);
List<User> users = named.query(
    "SELECT * FROM users WHERE id IN (:ids)",
    params,
    new UserRowMapper()
);

MapSqlParameterSource (fluent):

MapSqlParameterSource params = new MapSqlParameterSource()
    .addValue("name", "John")
    .addValue("email", "john@example.com")
    .addValue("status", "active");

int rows = named.update(
    "INSERT INTO users (name, email, status) VALUES (:name, :email, :status)",
    params
);

BeanPropertySqlParameterSource:

User user = new User();
user.setName("John");
user.setEmail("john@example.com");

SqlParameterSource params = new BeanPropertySqlParameterSource(user);
named.update("INSERT INTO users (name, email) VALUES (:name, :email)", params);

Batch with named parameters:

List<User> users = getUsers();
SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(users.toArray());
named.batchUpdate(
    "INSERT INTO users (name, email) VALUES (:name, :email)",
    batch
);

JdbcClient

Modern fluent API (Spring 6.1+). Supports both indexed and named parameters.

API

public interface JdbcClient {
    static JdbcClient create(DataSource ds) {}
    StatementSpec sql(String sql);

    interface StatementSpec {
        // Parameters
        StatementSpec param(Object value);
        StatementSpec param(int index, Object value);
        StatementSpec param(String name, Object value);
        StatementSpec params(Object... values);
        StatementSpec params(Map<String,?> params);
        StatementSpec paramSource(SqlParameterSource source);

        // Configuration
        StatementSpec withFetchSize(int size);
        StatementSpec withMaxRows(int max);
        StatementSpec withQueryTimeout(int timeout);

        // Execution
        <T> MappedQuerySpec<T> query(RowMapper<T> mapper);
        <T> MappedQuerySpec<T> query(Class<T> type);
        <T> T query(ResultSetExtractor<T> extractor);
        void query(RowCallbackHandler handler);
        ResultQuerySpec query();
        int update();
        int update(KeyHolder keyHolder);
    }

    interface MappedQuerySpec<T> {
        Optional<T> optional();
        T single();
        List<T> list();
        Set<T> set();
        Stream<T> stream();
    }

    interface ResultQuerySpec {
        Map<String,Object> singleRow();
        List<Map<String,Object>> listOfRows();
        List<Object> singleColumn();
        Object singleValue();
        Optional<Object> optionalValue();
        SqlRowSet rowSet();
    }
}

Patterns

Indexed parameters:

User user = client.sql("SELECT * FROM users WHERE id = ?")
    .param(123)
    .query((rs, row) -> new User(...))
    .single();

List<User> users = client.sql("SELECT * FROM users WHERE status = ? AND age > ?")
    .params("active", 18)
    .query((rs, row) -> new User(...))
    .list();

Named parameters:

User user = client.sql("SELECT * FROM users WHERE id = :userId")
    .param("userId", 123)
    .query((rs, row) -> new User(...))
    .single();

List<User> users = client.sql("SELECT * FROM users WHERE status = :status AND age >= :minAge")
    .param("status", "active")
    .param("minAge", 18)
    .query((rs, row) -> new User(...))
    .list();

Optional results:

Optional<User> user = client.sql("SELECT * FROM users WHERE email = ?")
    .param("unknown@example.com")
    .query((rs, row) -> new User(...))
    .optional();

if (user.isPresent()) {
    // Process user
}

Updates:

int rows = client.sql("UPDATE users SET status = ? WHERE id = ?")
    .params("inactive", 123)
    .update();

KeyHolder keyHolder = new GeneratedKeyHolder();
client.sql("INSERT INTO users (name, email) VALUES (?, ?)")
    .params("John", "john@example.com")
    .update(keyHolder);
Long id = keyHolder.getKey().longValue();

Map results:

Map<String, Object> row = client.sql("SELECT * FROM users WHERE id = ?")
    .param(123)
    .query()
    .singleRow();

List<Map<String, Object>> rows = client.sql("SELECT * FROM users WHERE status = ?")
    .param("active")
    .query()
    .listOfRows();

Streaming:

try (Stream<User> stream = client.sql("SELECT * FROM users")
    .query((rs, row) -> new User(...))
    .stream()) {
    stream.filter(u -> u.isActive())
          .forEach(this::process);
}

Choosing the Right API

ScenarioRecommended API
Simple positional parametersJdbcTemplate
Many parameters (3+)NamedParameterJdbcTemplate
Modern fluent styleJdbcClient
Optional handlingJdbcClient
Existing codebaseMatch existing pattern
High performanceAll perform equally

Advanced Patterns

Custom callbacks:

// ConnectionCallback
Boolean supports = jdbc.execute((ConnectionCallback<Boolean>) con ->
    con.getMetaData().supportsTransactions()
);

// PreparedStatementCallback
Integer count = jdbc.execute(
    "SELECT COUNT(*) FROM users WHERE status = ?",
    (PreparedStatementCallback<Integer>) ps -> {
        ps.setString(1, "active");
        ResultSet rs = ps.executeQuery();
        return rs.next() ? rs.getInt(1) : 0;
    }
);

ResultSetExtractor:

Map<Long, User> userMap = jdbc.query(
    "SELECT id, name, email FROM users",
    (ResultSetExtractor<Map<Long, User>>) rs -> {
        Map<Long, User> map = new LinkedHashMap<>();
        while (rs.next()) {
            Long id = rs.getLong("id");
            map.put(id, new User(id, rs.getString("name"), rs.getString("email")));
        }
        return map;
    }
);

RowCallbackHandler (side effects):

jdbc.query(
    "SELECT email FROM users WHERE status = ?",
    (RowCallbackHandler) rs -> emailService.send(rs.getString("email"), "Welcome!"),
    "new"
);

Configuration

JdbcTemplate jdbc = new JdbcTemplate(dataSource);
jdbc.setQueryTimeout(30);      // 30 seconds
jdbc.setFetchSize(1000);       // Fetch 1000 rows at a time
jdbc.setMaxRows(10000);        // Maximum 10000 rows

Spring Integration

@Configuration
public class DatabaseConfig {
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    public NamedParameterJdbcTemplate namedTemplate(JdbcTemplate jdbc) {
        return new NamedParameterJdbcTemplate(jdbc);
    }

    @Bean
    public JdbcClient jdbcClient(DataSource dataSource) {
        return JdbcClient.create(dataSource);
    }
}

@Service
public class UserService {
    private final JdbcTemplate jdbc;

    public UserService(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public User findById(Long id) {
        return jdbc.queryForObject(
            "SELECT * FROM users WHERE id = ?",
            (rs, row) -> new User(...),
            id
        );
    }
}