Перейти к содержанию

JdbcTemplate в Spring Boot

В этом руководстве узнаем, как использовать JdbcTemplate для запроса или сохранения данных в существующих таблицах базы данных, а затем предоставлять эти операции через REST API.

Используя JdbcTemplate, можно запускать запросы и хранимые процедуры из кода Java, что обеспечивает низкоуровневый доступ к базе данных.

Будем использовать учебную базу данных MySQL classicmodels. Ее можно найти здесь. Можно загрузить базу данных с этого сайта вместе с информацией об установке. Эта база данных на самом деле содержит несколько таблиц, но в этом руководстве будем использовать только таблицу Customers.

Создание проекта Spring Boot

Чтобы создать новый проект Spring Boot, воспользуемся Spring Initializr (https://start.spring.io/), который сгенерирует базовую структуру проекта Spring Boot. Вот pom.xml этого проекта:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.9</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.raven</groupId>
	<artifactId>spring-boot-jdbctemplate</artifactId>
	<version>1.0.0-SNAPSHOT</version>
	<name>spring-boot-jdbctemplate</name>
	<description>Spring Boot project with JDBCTemplate</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-rest</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui -->
		<dependency>
			<groupId>org.springdoc</groupId>
			<artifactId>springdoc-openapi-ui</artifactId>
			<version>1.6.9</version>
		</dependency>

	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

Были добавлены следующие зависимости:

  • Spring Boot DevTools – необходимые инструменты разработки;
  • Rest Repositories – для предоставления репозиториев Spring Data через REST;
  • JDBC API – API подключения к базе данных, который определяет, как клиент может подключаться и запрашивать базу данных;
  • MySQL Driver – драйвер JDBC для MySQL (для другой БД необходимо выбрать соответствующую зависимость);
  • Springdoc OpenAPI – для пользовательского интерфейса Swagger.

Подключение к базе данных

Установим некоторые настройки подключения к MySQL в файле application.properties:

application-description=@project.description@
application-version=@project.version@
logging.level.org.springframework.boot.autoconfigure=ERROR

api.response-codes.ok.desc=OK
api.response-codes.badRequest.desc=BAD_REQUEST
api.response-codes.notFound.desc=NOT_FOUND

## first db
spring.datasource.jdbcUrl=jdbc:mysql://172.17.0.2:3306/classicmodels?useSSL=false
spring.datasource.username=root
spring.datasource.password=admin@123

Конфигурация JdbcTemplate

Для настройки JdbcTemplate создали класс ApplicationConfiguration.java в пакете config:

package com.raven.jdbctemplate.config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class ApplicationConfiguration {

    @Bean(name = "dataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "jdbcTemplate")
    public JdbcTemplate jdbcTemplate1(@Qualifier("dataSource") DataSource ds) {
        return new JdbcTemplate(ds);
    }
}

Мы аннотировали этот класс с помощью @Configuration, что указывает на то, что этот класс имеет один или несколько бинов, которые можно использовать во всем проекте.

Мы объявили два бина (dataSource и jdbcTemplate), используя аннотацию @Bean. Этими бинами будет управлять контекст Spring, поэтому нам не нужно создавать новый экземпляр всякий раз, когда хотим его использовать.

Аннотация @ConfigurationProperties используется для чтения значений свойств spring.datasource из файла application.properties и построения источника данных.

Используя аннотацию @Qualifier, указываем бин datasource в качестве DataSource для создания нового экземпляра JdbcTemplate.

Модель клиента

Сначала создаем CustomerModel, которая будет содержать данные таблицы клиентов. Вот класс CustomerModel в пакете model:

package com.raven.jdbctemplate.model;

public class CustomerModel {

	private int customerNumber = 0;
	private String customerName = "";
	private String phone = "";
	private String address1 = "";
	private String city = "";
	private String country = "";

	public CustomerModel() {
		super();
	}

	public CustomerModel(int customerNumber, String customerName, String phone, String address1, String city,
			String country) {
		super();
		this.customerNumber = customerNumber;
		this.customerName = customerName;
		this.phone = phone;
		this.address1 = address1;
		this.city = city;
		this.country = country;
	}

	public int getCustomerNumber() {
		return customerNumber;
	}

	public void setCustomerNumber(int customerNumber) {
		this.customerNumber = customerNumber;
	}

	public String getCustomerName() {
		return customerName;
	}

	public void setCustomerName(String customerName) {
		this.customerName = customerName;
	}

	public String getPhone() {
		return phone;
	}

	public void setPhone(String phone) {
		this.phone = phone;
	}

	public String getAddress1() {
		return address1;
	}

	public void setAddress1(String address1) {
		this.address1 = address1;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}

	public String getCountry() {
		return country;
	}

	public void setCountry(String country) {
		this.country = country;
	}

	@Override
	public String toString() {
		return "CustomerModel [customerNumber=" + customerNumber + ", customerName=" + customerName + ", phone=" + phone
				+ ", address1=" + address1 + ", city=" + city + ", country=" + country + "]";
	}
}

Мы используем некоторые столбцы для сопоставления в CustomerModel, но можно добавить к сопоставлению другие столбцы из таблицы Customer.

Repository

Интерфейс CustomerRepository в пакете repository, который содержит список абстрактных методов, которые будут реализованы для выполнения CRUD-операций базы данных:

package com.raven.jdbctemplate.repository;

import java.util.List;
import java.util.Optional;

import com.raven.jdbctemplate.model.CustomerModel;

public interface CustomerRepository {
    // gets the total record count
    int count();

    // saves a customer
    int saveCustomer(CustomerModel customerModel);

    // updates an existing customer
    int updateCustomer(CustomerModel customerModel, int id);

    // deletes ann existing customer
    int deleteCustomer(int id);

    // get all customer
    List<CustomerModel> findAll();

    // get a customer by CustomerNumber
    Optional<CustomerModel> findByCustomerNumber(int id);
}

Реализация Repository

Теперь создаем класс CustomerJDBCRepository в пакете repository. Этот класс будет реализовывать интерфейс CustomerRepository и переопределять все методы работы CRUD:

package com.raven.jdbctemplate.repository;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;

import com.raven.jdbctemplate.model.CustomerModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

@Repository
public class CustomerJDBCRepository implements CustomerRepository {

    @Qualifier("jdbcTemplate")
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public int count() {
        return jdbcTemplate
                .queryForObject("select count(*) from customers", Integer.class);
    }

    @Override
    public int saveCustomer(CustomerModel customerModel) {
        String sql = "INSERT INTO customers(customerName, contactLastName, contactFirstName, phone, addressLine1, city, country) " +
                "VALUES(?,?,?,?,?,?,?)";
        return jdbcTemplate.update(sql, customerModel.getCustomerName(),
                customerModel.getCustomerName().split(" ")[1],
                customerModel.getCustomerName().split(" ")[0],
                customerModel.getPhone(),
                customerModel.getAddress1(),
                customerModel.getCity(),
                customerModel.getCountry());
    }

    @Override
    public int updateCustomer(CustomerModel customerModel, int id) {
        String sql = "UPDATE customers " +
                "SET customerName= ?, contactLastName= ?, contactFirstName= ?, phone=?, addressLine1=?, " +
                "city= ?, country= ? WHERE customerNumber= ?;";
        return jdbcTemplate.update(sql, customerModel.getCustomerName(),
                customerModel.getCustomerName().split(" ")[1],
                customerModel.getCustomerName().split(" ")[0],
                customerModel.getPhone(),
                customerModel.getAddress1(),
                customerModel.getCity(),
                customerModel.getCountry(),
                id);
    }

    @Override
    public int deleteCustomer(int id) {
        String sql = "DELETE FROM customers WHERE customerNumber = ?";
        return jdbcTemplate.update(sql, id);
    }

    @Override
    public List<CustomerModel> findAll() {
        String sql = "select customerNumber, customerName, phone, addressLine1, city, country from customers";
        return jdbcTemplate.query(sql, new CustomerRowMapper());
    }

    @Override
    public Optional<CustomerModel> findByCustomerNumber(int id) {
        String sql = "select customerNumber, customerName, phone, addressLine1, city, country from customers where customerNumber = ?";
        return jdbcTemplate.query(sql, new CustomerRowMapper(), id).stream().findFirst();
    }

    private class CustomerRowMapper implements RowMapper<CustomerModel> {

        @Override
        public CustomerModel mapRow(ResultSet rs, int rowNum) throws SQLException {
            return new CustomerModel(rs.getInt("customerNumber"),
                    rs.getString("customerName"),
                    rs.getString("phone"),
                    rs.getString("addressLine1"),
                    rs.getString("city"),
                    rs.getString("country"));
        }
    }
}

Внутри этого класса объявили внутренний класс CustomerRowMapper, реализующий интерфейс RowMapper. RowMapper предоставляет метод для сопоставления строк с объектом, поэтому переопределяем метод mapRow(), который сопоставляет строки с CustomerModel из ResultSet. CustomerRowMapper используется для извлечения из базы данных информации об одном клиенте или списка данных о клиентах.

jdbcTemplate.queryForObject() используется для получения одной строки или значения. Здесь получаем только значение count(*).

Для извлечения данных использовали метод query() JdbcTemplate, который извлекает записи из базы данных на основе SQL-запроса и сопоставляет запись с объектом CustomerModel с помощью CustomerRowMapper.

Функция update() JdbcTemplate используется для выполнения операций вставки, обновления и удаления и принимает в качестве аргументов строку, содержащую SQL-запрос и значения.

Controller

Создадим класс CustomerController в пакете controller:

package com.raven.jdbctemplate.controller;

import com.raven.jdbctemplate.model.CustomerModel;
import com.raven.jdbctemplate.repository.CustomerJDBCRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/customer")
@Tag(description = "APIs related with Customers", name = "Customer")
public class CustomerController {

    private CustomerJDBCRepository customerJDBCRepository;

    @Autowired
    public CustomerController(CustomerJDBCRepository customerJDBCRepository) {
        this.customerJDBCRepository = customerJDBCRepository;
    }

    @Operation(summary = "Total record count",
            description = "Get total Customer count")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200",
                    description = "OK",
                    content = {@Content(mediaType = "application/json",
                            array = @ArraySchema(schema = @Schema(implementation = CustomerModel.class)))}),
            @ApiResponse(responseCode = "400",
                    description = "BAD_REQUEST"),
            @ApiResponse(responseCode = "404",
                    description = "NOT_FOUND")})
    @RequestMapping(value = "/getRecordNumber", method = RequestMethod.GET)
    @ResponseBody
    public int getRecordNumber() {
        return customerJDBCRepository.count();
    }

    @Operation(summary = "All customer",
            description = "Get all customer details")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200",
                    description = "OK",
                    content = {@Content(mediaType = "application/json",
                            array = @ArraySchema(schema = @Schema(implementation = CustomerModel.class)))}),
            @ApiResponse(responseCode = "400",
                    description = "BAD_REQUEST"),
            @ApiResponse(responseCode = "404",
                    description = "NOT_FOUND")})
    @RequestMapping(value = "/getAllCustomer", method = RequestMethod.GET)
    @ResponseBody
    public List<CustomerModel> getAllCustomer() {
        return customerJDBCRepository.findAll();
    }

    @Operation(summary = "Customer details by customer number",
            description = "Customer details by customer number")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200",
                    description = "OK",
                    content = {@Content(mediaType = "application/json",
                            array = @ArraySchema(schema = @Schema(implementation = CustomerModel.class)))}),
            @ApiResponse(responseCode = "400",
                    description = "BAD_REQUEST"),
            @ApiResponse(responseCode = "404",
                    description = "NOT_FOUND")})
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    @ResponseBody
    public Optional<CustomerModel> getCustomerByNumber(@PathVariable("id") int id) {
        return customerJDBCRepository.findByCustomerNumber(id);
    }

    @Operation(summary = "Save customer details",
            description = "Save customer details")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200",
                    description = "OK"),
            @ApiResponse(responseCode = "400",
                    description = "BAD_REQUEST"),
            @ApiResponse(responseCode = "404",
                    description = "NOT_FOUND")})
    @RequestMapping(value = "/save", method = RequestMethod.POST)
    @ResponseBody
    public int save(@Valid @RequestBody CustomerModel customerModel) {
        return customerJDBCRepository.saveCustomer(customerModel);
    }

    @Operation(summary = "Update customer details",
            description = "Update customer details using customer number")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200",
                    description = "OK"),
            @ApiResponse(responseCode = "400",
                    description = "BAD_REQUEST"),
            @ApiResponse(responseCode = "404",
                    description = "NOT_FOUND")})
    @RequestMapping(value = "/update/{id}", method = RequestMethod.PUT)
    @ResponseBody
    public int update(@Valid @RequestBody CustomerModel customerModel, @PathVariable("id") int id) {
        return customerJDBCRepository.updateCustomer(customerModel, id);
    }

    @Operation(summary = "Delete a customer",
            description = "Delete a customer using customer number")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200",
                    description = "OK"),
            @ApiResponse(responseCode = "400",
                    description = "BAD_REQUEST"),
            @ApiResponse(responseCode = "404",
                    description = "NOT_FOUND")})
    @RequestMapping(value = "/delete/{id}", method = RequestMethod.DELETE)
    @ResponseBody
    public int delete(@PathVariable("id") int id) {
        return customerJDBCRepository.deleteCustomer(id);
    }
}

Класс контроллера помечен аннотацией @RestController. Это сообщает Spring Boot, что это служба на основе REST, и она автоматически сериализует или десериализует данные запроса/ответа в JSON.

Мы указали базовый путь запроса как /api/customer в @RequestMapping. Аннотация @RequestMapping сообщает контейнеру Spring, что к этой службе может получить доступ указанная конечная точка HTTP.

@Tag – это аннотация Open API, используемая для настройки сведений о ресурсе, таких как имя ресурса, описание.

@Operation также является аннотацией Open API, используемой для настройки имени и описания API.

@ApiResponses – это еще одна аннотация Open API для указания формата ответа API, такого как форматы ответа об успехе и ошибке и т. д.

Пользовательский интерфейс Swagger доступен по адресу http://localhost:8080/swagger-ui/index.html, где можно увидеть список API:

Исходный код можно скачать здесь.

Оригинал