springboot整合jdbc--spring-data-jdbc入门

一、概述

1.1 Spring Data介绍

对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),Spring Boot 底层都是采用 Spring Data 的方式进行统一处理。

Spring Boot 底层都是采用 Spring Data 的方式进行统一处理各种数据库,Spring Data 也是 Spring 中与 Spring Boot、Spring Cloud 等齐名的知名项目。

1.2JDBC概述

JDBC(Java Data Base Connectivity):提供了一组Java程序访问关系数据库的API,使ava程序可以与任何支持SQL标准的数据库交互。

JDBC优点

  • ·简洁的SQL处理
  • ·针对大型数据的良好性能
  • ·接口语法简单
  • ·非常适合小型应用程序

JDBC缺点

  • ·SQL语句以硬编码写于代码中,不利于维护移植
  • 需了解并掌握数据库技术
  • ·需手动实现封装
  • ·大型项目维护复杂,编程开销大

1.3补充

自动提示

idea中添加database视图并添加数据源,可使项目支持对数据源操作的自动提示

如果没有自动语法提示 在编写SQL代码处 alt+enter打开语法提示

映射关系

1.4ORM介绍

ORM,即对象关系映射(Object-Relational Mapping),是一种编程技术,用于在面向对象编程语言和关系型数据库之间建立数据转换的桥梁。
ORM优点:

  • 业务逻辑访问、操作对象而非数据库记录
  • 无需关心底层数据库,通过面向对象逻辑隐藏具体SQL实现细节
  • 实体基于业务关系而非数据表结构关系
  • 适应应用的快速开发与迭代
  • DO(Data Object)。与数据库表结构一一对应的
  • DTO(Data Transfer Object)。数据传输对象,Service 或 Manager 向外传输的对象。即,通过Service层按需求组织DO数据封装到DTO对象
  • VO(View Object)。显示层对象,向视图传输的对象。可将DTO对象进一步封装/转换/脱敏 所有仅用于封装数据的,仅包含属性不包含处理逻辑的类,统称POJO类(贫血模式)
  • 依据Java开发手册,基于项目的复杂程度具体设计 课程 Entity类 == DO类

二、整合JDBC步骤

2.1创建测试项目

bash 复制代码
 <!-- junit-platform-launcher -->
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-launcher</artifactId>
            <scope>test</scope>
        </dependency>

2.2编写yaml配置文件连接数据库

XML 复制代码
spring:
  datasource:
    url: 'jdbc:mysql:
    createDatabaseIfNotExist=true
    &serverTimezone=Asia/Shanghai'
    username: 
    password: 
  sql:
    init:
      mode: always

logging:
  level:
    sql: debug
    com:
      example: debug
  pattern:
    console: '%-5level %C.%M[%line] - %msg%n'

测试

java 复制代码
package com.yanyu.springjdbc;


import jakarta.annotation.Resource;
import lombok.extern.java.Log;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@SpringBootTest
@Log
class test1 {

    //DI注入数据源
    @Resource
    DataSource dataSource;

    @Test
    public void contextLoads() throws SQLException {
        //看一下默认数据源
        System.out.println(dataSource.getClass());
        //获得连接
        Connection connection =   dataSource.getConnection();
        System.out.println(connection);
        //关闭连接
        connection.close();

    }
}

HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀;

可以使用 spring.datasource.type 指定自定义的数据源类型,值为 要使用的连接池实现的完全限定名。

2.3创建数据表

sql 复制代码
# 创建 `user` 表,附带必要注释
CREATE TABLE IF NOT EXISTS `user`
(
    id BIGINT(19) NOT NULL PRIMARY KEY,  # 用户ID
    name VARCHAR(45),  # 用户名
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,  # 创建时间
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  # 更新时间
);


# 创建 `address` 表,附带必要注释
CREATE TABLE IF NOT EXISTS `address`
(
    id BIGINT(19) NOT NULL PRIMARY KEY,  # 地址ID
    detail VARCHAR(45),  # 地址详情
    user_id BIGINT(19),  # 用户ID
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,  # 创建时间
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,  # 更新时间
    INDEX (user_id)  # 用户ID索引
);


# 创建 `account` 表,附带必要注释

CREATE TABLE IF NOT EXISTS `account`
(
    id BIGINT(19) NOT NULL PRIMARY KEY,  # 账户ID
    name VARCHAR(20),  # 账户名
    balance FLOAT,  # 账户余额
    version INT DEFAULT 0  # 版本号
);


# 创建 `github_user` 表,附带必要注释
CREATE TABLE IF NOT EXISTS `github_user`
(
    id BIGINT(19) NOT NULL PRIMARY KEY,  # GitHub用户ID
    name VARCHAR(20),  # GitHub用户名
    followers INT DEFAULT 0,  # 粉丝数
    stars INT DEFAULT 0,  # 获得的星标数
    gender VARCHAR(10),  # 性别
    repos INT DEFAULT 0  # 仓库数
);

声明执行初始化脚本。默认扫描resources下schema.sql为数据库初始化脚本;data.sql为数据初始化脚本。支持为不同数据库单独声明
注意:

需要调用业务逻辑的初始化无效。例如,初始化管理员账号,但初始化密码必须经过业务逻辑编码,才能保存在数据表。因此,数据的初始化可以由业务逻辑完成,后期讨论。

2.4实体类dox

相关说明

java 复制代码
package com.example.jdbcexamples.dox;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.*;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {
    @Id
    @CreatedBy
    private String id;
    private String name;
    @ReadOnlyProperty
    @CreatedDate
    private LocalDateTime createTime;
    @ReadOnlyProperty
    @LastModifiedDate
    private LocalDateTime updateTime;
}
  • @Id注解用于标记实体类的主键属性,它告诉ORM框架该属性对应数据库表的主键列
  • 当使用ORM(对象关系映射)框架时,@CreatedBy注解可以帮助自动填充创建记录的用户的ID或用户名。这样,在保存实体对象到数据库时,框架会自动将当前登录用户的信息存储在相应的字段中。
  • @Version注解在Java中用于表示实体类的版本属性。它通常用于乐观锁机制,以确保在并发环境下对数据库记录的更新是安全的。
  • @ReadOnlyProperty注解在Java中用于表示实体类的属性是只读的。它通常用于ORM框架中,以确保该属性在数据库操作中不会被修改。
  • 当使用ORM框架时,@CreatedDate注解可以与实体类一起使用,以指示某个属性是创建日期,即记录被保存到数据库时的日期和时间
  • 当使用ORM框架时,@LastModifiedDate注解可以与实体类一起使用,以指示某个属性是最后修改日期,即记录被更新到数据库时的日期和时间。

2.5工具类

java 复制代码
// 导入Java时间格式化类库
import java.time.format.DateTimeFormatter;

// 定义一个日期时间格式化工具类
public class DateTimeFormatterUtils {
    // 定义一个私有的静态常量,用于存储日期时间格式化器实例
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    // 定义一个公共的静态方法,用于获取日期时间格式化器实例
    public static DateTimeFormatter getFormatter() {
        return formatter;
    }
}

三、JDBCTemplate

3.1概述

1、有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库;

2、即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。

3、数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。

4、Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用

5、JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类

3.2 方法说明

JdbcTemplate主要提供以下几类方法:

  • execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;

  • update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;

  • query方法及queryForXXX方法:用于执行查询相关语句;

  • call方法:用于执行存储过程、函数相关语句。

3.3实现

java 复制代码
package com.yanyu.springjdbc.controller;

import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;


import java.util.Date;
import java.util.List;
import java.util.Map;
@RestController
public class JDBCController {
        @Resource
        JdbcTemplate jdbcTemplate;
        @GetMapping("/list")
        public List<Map<String, Object>> userList() {
            String sql = "select * from user";
            List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
            return maps;
        }

        @GetMapping("/add")
        public String addUser() {
            // 插入语句,注意时间问题
            String sql = "insert into user(name, create_time, update_time) values ('yanyu', ?, ?)";
            jdbcTemplate.update(sql, new Date(), new Date());
            return "addOk";
        }

        @GetMapping("/update/{id}")
        public String updateUser(@PathVariable("id") int id) {
            // 更新语句
            String sql = "update user set name=?, update_time=? where id=" + id;
            // 数据
            Object[] objects = new Object[2];
            objects[0] = "yanyu1";
            objects[1] = new Date();
            jdbcTemplate.update(sql, objects);
            return "updateOk";
        }

        @GetMapping("/delete/{id}")
        public String delUser(@PathVariable("id") int id) {
            // 删除语句
            String sql = "delete from user where id=?";
            jdbcTemplate.update(sql, id);
            return "deleteOk";
        }
}

测试1

bash 复制代码
GET http://localhost:8080/list

测试2

bash 复制代码
GET http://localhost:8080/update/1

四、CrudRepository

4.1概述

CrudRepository<T, ID>接口,提供了针对DO类的基本CRUD操作方法。T,操作的DO类型,ID,主键类型 T save(S entity)方法,默认保存全部属性值,值为null时也会保存到数据库,因此会覆盖数据库设置的default值 T save(S entity)方法,当保存对象id属性非空时,执行update更新方法,同样会覆盖全部属性值

4.2常见方法

  • Optional<T> findById(ID id)
  • long count()
  • Iterable<S> findAll()/findAllById(Iterable<ID> ids)
  • Iterable<S> saveAll(Iterable<S> entities)
  • void delete(T entity)/deleteById(ID id)/deleteAll()/deleteAllById(Iterable<ID> ids)
  • boolean existsById(ID id)

4.3实现

java 复制代码
package com.example.jdbcexamples.repository;

import com.example.jdbcexamples.dox.User;
import com.example.jdbcexamples.dto.AddressUser;
import com.example.jdbcexamples.dto.UserAddress;
import com.example.jdbcexamples.dto.UserAddress3;
import com.example.jdbcexamples.mapper.UserAddress3ResultSetExtractor;
import com.example.jdbcexamples.mapper.UserAddressResultSetExtractor;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserRepository extends CrudRepository<User, String> {
    /**
     * 建议显式声明映射名称对应DTO中属性名称,避免冲突
     *
     * @param uid
     * @return
     */
    @Query("""
            select a.id as id, a.create_time as create_time, a.update_time as update_time, name, detail, a.user_id as user_id 
            from user u join address a 
            on u.id = a.user_id 
            where u.id=:uid
            """)
    List<AddressUser> findAllByUid(String uid);


    @Query(value = "select * from address a join user u on u.id = a.user_id where u.id=:uid",
            resultSetExtractorClass = UserAddress3ResultSetExtractor.class)
    UserAddress3 findUserAddress3(String uid);

    @Query(value = "select * from user u join address a on u.id = a.user_id where u.id=:uid",
    resultSetExtractorClass = UserAddressResultSetExtractor.class)
    UserAddress findUserAddress(String uid);
}

4.4ResultSetExtractor<T>接口

ResultSetExtractor<T>接口,自定义结果的映射实现。例如将多条记录映射为一个对象组装一个集合 T extractData(ResultSet rs),重写映射规则方法。传入的ResultSet为整个结果集对象,与原生JDBC类似,需手动移动游标遍历结果集。

  • resultSetExtractorClass属性指定了一个结果集提取器类,用于将查询结果转换为Java对象。例如
java 复制代码
package com.example.jdbcexamples.mapper;

import com.example.jdbcexamples.dox.Address;
import com.example.jdbcexamples.dox.User;
import com.example.jdbcexamples.dto.UserAddress;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ResultSetExtractor;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Slf4j
public class UserAddressResultSetExtractor implements ResultSetExtractor<UserAddress> {

    /**
     * 从ResultSet中提取数据,将结果映射到UserAddress对象
     *
     * @param rs SQL查询结果集
     * @return UserAddress对象,包含User和Address列表
     * @throws SQLException        如果提取数据时发生SQL异常
     * @throws DataAccessException 如果提取数据时发生访问数据异常
     */
    @Override
    public UserAddress extractData(ResultSet rs) throws SQLException, DataAccessException {
        // 初始化User对象为null,以便在循环中检查是否已提取用户信息
        User user = null;
        // 初始化地址列表
        List<Address> addresses = new ArrayList<>();
        
        // 遍历ResultSet中的每一行数据
        while (rs.next()) {
            // 如果user对象尚未初始化,则提取用户信息并构建User对象
            if (user == null) {
                user = User.builder()
                        .id(rs.getString("u.id"))
                        .name(rs.getString("name"))
                        .createTime(rs.getObject("u.create_time", LocalDateTime.class))
                        .updateTime(rs.getObject("u.update_time", LocalDateTime.class))
                        .build();
            }
            
            // 提取地址信息并构建Address对象,然后将其添加到地址列表中
            Address a = Address.builder().id(rs.getString("a.id"))
                    .detail(rs.getString("detail"))
                    .userId(rs.getString("user_id"))
                    .createTime(rs.getObject("a.create_time", LocalDateTime.class))
                    .updateTime(rs.getObject("a.update_time", LocalDateTime.class))
                    .build();
            addresses.add(a);
        }
        
        // 构建UserAddress对象,包含提取的用户信息和地址列表
        return UserAddress.builder()
                .user(user)
                .addresses(addresses)
                .build();
    }
}

4.5保存

spring-data-jdbc的更新操作,同样基于save()方法。内部通过判断主键是否存在执行插入/更新操作

save()方法会更新全部属性字段。即,对象的空值属性也会更新!!因此,更新局部属性数据时,必须先查询出全部,合并,再执行更新操作,与JPA相同。mybatis支持仅更新非null属性

4.6乐观锁与悲观锁

数据库概念

数据库事务并发操作带来的数据不一致性现象

  • 幻读(Phantom Reads)
  • 脏读(Dirty Reads)
  • 重复读(Repeatable Reads)

锁介绍

乐观锁实现(Optimistic),能够同时保证高并发和高可伸缩性

应用级别的版本检查(Version Checking)

悲观锁实现(Pessimistic) 指定执行底层数据库事务隔离级别

乐观锁

Optimistic Locking原理:为每条数据创建数据版本值;加载时将同时加载数据版本值;修改时比较数据版本,如果值不同则拒绝更新;修改后增加数据版本值

相关推荐
路在脚下@1 小时前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
啦啦右一1 小时前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
森屿Serien1 小时前
Spring Boot常用注解
java·spring boot·后端
苹果醋33 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
盛派网络小助手3 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
∝请叫*我简单先生4 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl
荆州克莱4 小时前
mysql中局部变量_MySQL中变量的总结
spring boot·spring·spring cloud·css3·技术
zquwei5 小时前
SpringCloudGateway+Nacos注册与转发Netty+WebSocket
java·网络·分布式·后端·websocket·网络协议·spring
dessler5 小时前
Docker-run命令详细讲解
linux·运维·后端·docker
火烧屁屁啦5 小时前
【JavaEE进阶】初始Spring Web MVC
java·spring·java-ee