MTI TEK
Spring Framework | Spring JDBC

Spring Persistence with JDBC

Spring's JDBC support eliminates boilerplate Connection, Statement, and ResultSet lifecycle management while keeping SQL explicit and developer-controlled — no ORM magic, no entity state tracking, no lazy-loading surprises. JdbcTemplate / JdbcOperations are the primary entry points: they handle connection acquisition from the DataSource, statement execution, exception translation to Spring's DataAccessException hierarchy, and resource cleanup.

JdbcTemplate vs JdbcOperations

Transaction Management

H2 Console

JDBC URL: jdbc:h2:mem:mtitek-spring-jdbc
Username: sa
Password: (empty)

Dependencies and Auto-configuration

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-h2console</artifactId>
</dependency>
# application.properties
spring.application.name=mtitek-spring-jdbc

spring.datasource.name=mtitek-spring-jdbc
spring.datasource.generate-unique-name=false

Schema and Seed Data Initialization

// src/main/resources/schema.sql
create table if not exists AppUser (
  id identity,
  name varchar(10) not null
);

create table if not exists AppProfile (
  id varchar(1) not null PRIMARY KEY,
  name varchar(20) not null,
  role varchar(10) not null
);

create table if not exists AppUserProfileRef (
  appUserId bigint not null,
  appProfileId varchar(1) not null
);

alter table AppUserProfileRef add foreign key (appUserId) references AppUser(id);
alter table AppUserProfileRef add foreign key (appProfileId) references AppProfile(id);

Domain Model

@Data
public class AppUser {
    private Long id;

    @NotNull
    @Size(min = 3, message = "Name must be at least 3 characters long")
    private String name;

    @Size(min = 1, message = "You must choose at least 1 appProfile")
    private List<AppProfile> appProfiles = new ArrayList<>();

    public void addAppProfile(AppProfile appProfile) {
        this.appProfiles.add(appProfile);
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
public class AppProfile {
    private String id;
    private String name;
    private Role role;

    public enum Role {
        USER, ADMIN, SUPPORT;
    }
}

AppProfileJdbcRepository — Query and CRUD

@Repository
public class AppProfileJdbcRepository implements AppProfileRepository {
    private JdbcTemplate jdbcTemplate;
    private JdbcOperations jdbcOperations;

    public AppProfileJdbcRepository(JdbcTemplate jdbcTemplate, JdbcOperations jdbcOperations) {
        this.jdbcTemplate = jdbcTemplate;
        this.jdbcOperations = jdbcOperations;
    }

    @Override
    public List<AppProfile> findAll() {
        return jdbcTemplate.query("select id, name, role from AppProfile", this::mapRowToAppProfile);
    }

    @Override
    public Optional<AppProfile> findById(String id) {
        List<AppProfile> results = jdbcTemplate.query(
            "select id, name, role from AppProfile where id=?",
            this::mapRowToAppProfile, id);
        return results.size() == 0 ? Optional.empty() : Optional.of(results.get(0));
    }

    private AppProfile mapRowToAppProfile(ResultSet row, int rowNum) throws SQLException {
        return new AppProfile(row.getString("id"), row.getString("name"),
                AppProfile.Role.valueOf(row.getString("role")));
    }

    @Override
    public void save(AppProfile appProfile) {
        jdbcOperations.update("insert into AppProfile (id, name, role) values (?, ?, ?)",
                appProfile.getId(), appProfile.getName(), appProfile.getRole().name());
    }

    @Override
    public void update(AppProfile appProfile) {
        jdbcOperations.update("update AppProfile set name=?, role=? where id=?",
                appProfile.getName(), appProfile.getRole().name(), appProfile.getId());
    }

    @Override
    public void delete(AppProfile appProfile) {
        jdbcOperations.update("delete from AppProfile where id = ?", appProfile.getId());
    }
}

AppUserJdbcRepository — Insert with Generated Keys

@Repository
public class AppUserJdbcRepository implements AppUserRepository {
    private JdbcOperations jdbcOperations;

    public AppUserJdbcRepository(JdbcOperations jdbcOperations) {
        this.jdbcOperations = jdbcOperations;
    }

    @Override
    @Transactional
    public AppUser save(AppUser appUser) {
        PreparedStatementCreatorFactory pscf = new PreparedStatementCreatorFactory(
                "insert into AppUser (name) values (?)", Types.VARCHAR);
        pscf.setReturnGeneratedKeys(true);
        PreparedStatementCreator psc = pscf.newPreparedStatementCreator(Arrays.asList(appUser.getName()));

        GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcOperations.update(psc, keyHolder);

        long appUserId = keyHolder.getKey().longValue();
        appUser.setId(appUserId);

        saveAppProfiles(appUserId, appUser.getAppProfiles());

        return appUser;
    }

    private void saveAppProfiles(long appUserId, List<AppProfile> appProfiles) {
        for (AppProfile appProfile : appProfiles) {
            jdbcOperations.update(
                "insert into AppUserProfileRef (appUserId, appProfileId) values (?, ?)",
                appUserId, appProfile.getId());
        }
    }
}

MVC Integration and Session-Scoped Model

@Controller
@RequestMapping("/appProfiles")
@SessionAttributes("appUser")
public class AppProfileController {
    @ModelAttribute
    public void addAppProfilesToModel(Model model) {
        List<AppProfile> appProfiles = appProfileRepository.findAll();
        model.addAttribute("profiles", appProfiles);

        Role[] roles = AppProfile.Role.values();
        for (Role role : roles) {
            model.addAttribute(role.toString().toLowerCase(), filterByRole(appProfiles, role));
        }
    }

    @ModelAttribute(name = "appUser")
    public AppUser appUser() {
        return new AppUser();
    }

    @PostMapping("/appUsers")
    public String addAppProfile(@RequestParam(required = false) String appProfileId,
            @ModelAttribute AppUser appUser, Model model) {
        Optional<AppProfile> appProfile = appProfileRepository.findById(appProfileId);
        if (appProfile.isEmpty()) {
            model.addAttribute("errorMessage", "Invalid profile id: " + appProfileId);
            return "appUserProfileForm";
        }
        appUser.addAppProfile(appProfile.get());
        return "redirect:/appUsers/user";
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public String handleIllegalArgumentException(IllegalArgumentException ex, Model model) {
        model.addAttribute("errorMessage", ex.getMessage());
        return "appUserProfileForm";
    }
}