一、概述
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原理:为每条数据创建数据版本值;加载时将同时加载数据版本值;修改时比较数据版本,如果值不同则拒绝更新;修改后增加数据版本值