Skip to content

Quick Start

Here is how to go from an empty project to a working mapper in five minutes using plain JDBC—no Spring, no XML. Everything here comes straight from the larkbatis-sample module in the core repository.

1 · A result class

An ordinary Java bean with a no-arg constructor and standard getters/setters. No base class, no special annotations, and no interfaces to implement:

User.java
package com.example.app;

import java.time.Instant;

public class User {

    private long id;
    private String name;
    private String email;
    private Instant createdAt;

    public long getId() { return id; }
    public void setId(long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public Instant getCreatedAt() { return createdAt; }
    public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}

Column names map to properties using snake_casecamelCase, and this happens at build time, every time. created_at matches setCreatedAt. If that convention doesn't fit, alias the column in your SQL or define a <resultMap>.

2 · A mapper interface

UserMapper.java
package com.example.app;

import io.github.larkbatis.annotations.Insert;
import io.github.larkbatis.annotations.Options;
import io.github.larkbatis.annotations.Param;
import io.github.larkbatis.annotations.Select;
import java.util.List;

public interface UserMapper {

    @Select("SELECT id, name, email, created_at FROM users WHERE id = #{id}")
    User findById(long id);

    @Select("SELECT id, name, email, created_at FROM users ORDER BY id")
    List<User> findAll();

    @Select("SELECT COUNT(*) FROM users WHERE name LIKE #{pattern}")
    long countByName(@Param("pattern") String pattern);

    @Insert("INSERT INTO users (name, email, created_at) VALUES (#{name}, #{email}, #{createdAt})")
    @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
    int insert(User u);
}

You don't need a @Mapper annotation here: the processor picks up any interface with statement annotations on its own. @Mapper is only needed when an interface has all its statements in mapper XML, since javac wouldn't otherwise pass it to the processor.

3 · Compile

$ ./gradlew compileJava

Three source files are generated next to your code in the same package:

Generated What it is
UserMapper$$Impl The implementation. Plain JDBC, one method per statement
UserRow Row reader for User. One per result class, shared by every query returning it
LarkBatisMappers Static factory for all mappers in the compilation

These are real Java source files, and stepping through them in your IDE is encouraged. See Generated Code.

If nothing is generated

Check that larkbatis-processor is configured under annotationProcessor (not implementation), and make sure you are compiling with javac. See Troubleshooting.

4 · Wire it up and run

LarkBatisSession is the only dependency a generated mapper needs: it borrows a Connection, returns it, and translates SQLExceptions. The standalone version takes a DataSource:

SampleApp.java
package com.example.app;

import io.github.larkbatis.runtime.JdbcLarkBatisSession;
import io.github.larkbatis.runtime.LarkBatisTx;
import java.time.Instant;
import javax.sql.DataSource;

public class SampleApp {

    public static void main(String[] args) {
        DataSource ds = /* HikariCP, H2, or whatever pool you use */;

        JdbcLarkBatisSession session = new JdbcLarkBatisSession(ds);
        UserMapper mapper = LarkBatisMappers.userMapper(session);

        User u = new User();
        u.setName("Ada");
        u.setEmail("ada@example.com");
        u.setCreatedAt(Instant.now());

        try (LarkBatisTx tx = session.begin()) {
            mapper.insert(u);
            tx.commit();
        }

        System.out.println(u.getId());              // populated by useGeneratedKeys
        System.out.println(mapper.findById(u.getId()));
        System.out.println(mapper.countByName("A%"));
    }
}

session.begin() opens a transaction scope designed for try-with-resources. Calling commit() is a vote: the real commit happens when the outermost block closes. If any scope exits without voting to commit, the whole transaction rolls back. See Transactions.

5 · Inspect what was generated

Open UserMapper$$Impl.java in your IDE. The SQL statement is a static final String where #{} has been replaced with ?, parameters are bound by index, and rows are read positionally because the generator parsed the column list:

private static final String SQL_findById =
        "SELECT id, name, email, created_at FROM users WHERE id = ?";

@Override
public User findById(long id) {
    Connection c = s.conn();
    try (PreparedStatement ps = c.prepareStatement(SQL_findById)) {
        ps.setLong(1, id);
        try (ResultSet rs = ps.executeQuery()) {
            return rs.next() ? UserRow.read(rs) : null;
        }
    } catch (SQLException e) {
        throw s.translate(e, SQL_findById);
    } finally {
        s.release(c);
    }
}
UserRow.java (excerpt)
public static User read(ResultSet rs) throws SQLException {
    User u = new User();
    u.setId(rs.getLong(1));
    u.setName(rs.getString(2));
    u.setEmail(rs.getString(3));
    u.setCreatedAt(JdbcCodec.instant(rs, 4));
    return u;
}

Every column index and getter/setter call was determined at build time. Nothing here inspects types at runtime, resolves property names, or looks up registries.

Why Connection is not in try-with-resources

Only s.release(c) knows whether the connection can actually be closed. If you're inside an active transaction (Spring or LarkBatis), the connection belongs to that transaction and closing it would break it. See Transactions for details.

Next steps