Spring Boot实践三 --数据库访问(JdbcTemplate、mybatis、JPA Hibernate)

一,使用JdbcTemplate访问MySQL数据库

1,确认本地已正确安装mysql

  • 按【win+r】快捷键打开运行;
  • 输入services.msc,点击【确定】;
  • 在打开的服务列表中查找mysql服务,如果没有mysql服务,说明本机没有安装mysql,按如下方式进行安装:

(1)点击mysql安装包下载链接:https://dev.mysql.com/downloads/mysql/,点击直接下载即可;
(2)解压后,在bin目录同级下创建一个文件,命名为my.ini
在这里插入图片描述
内容如下:

[mysqld]
# 设置3306端口
port=3306
# 设置mysql的安装目录 ---这里输入你安装的文件路径----
basedir=D:\mysql\mysql_install
# 设置mysql数据库的数据的存放目录
datadir=D:\mysql\data
# 允许最大连接数
max_connections=200
# 允许连接失败的次数。
max_connect_errors=10
# 服务端使用的字符集默认为utf8
character-set-server=utf8
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
# 默认使用“mysql_native_password”插件认证
#mysql_native_password
default_authentication_plugin=mysql_native_password
[mysql]
# 设置mysql客户端默认字符集
default-character-set=utf8
[client]
# 设置mysql客户端连接服务端时默认使用的端口
port=3306
default-character-set=utf8

(3) 打开cmd进入mysql的bin文件下(D:\mysql\mysql-8.0.16-winx64),执行命令:

mysqld --initialize --console

在这里插入图片描述

注意:安装mysql 安装完成后Mysql会有一个随机初始密码,一定要保存!
如果exe安装时有设置密码,那就用那个密码。

(4)接下来执行以下命令:

// 执行mysql安装
mysqld --install mysql

// 使用命令提示符启动MySQL服务
net start mysql

//进入mysql,回车后输入上面安装时保存的初始密码
mysql -uroot -p

//修改密码为1234
ALTER USER 'root'@'localhost' IDENTIFIED BY '1234';

// 查看MySQL服务状态
status

//quit退出mysql后用以下命令也可以查询:
mysql -u root -p -e"status;"

(5)配置环境变量:

在这里插入图片描述

2,配置IDEA访问MySQL数据库:

1,打开cmd,进入mysql的bin目录,执行以下命令创建数据库:

//进入mysql的bin目录
D:
cd mysql\mysql-8.0.16-winx64\bin

//登录mysql,注意-p后面无空格
mysql -uroot -p1234

// 创建 mydatabase
 create database mydatabase;
 
 // 查看databases
 show databases;
 
 // 切换到mydatabase
 use mydatabase;
 
 // 创建table `t_user`, 有id、username和password三项
 CREATE TABLE `t_user` (`id` int NOT NULL AUTO_INCREMENT COMMENT '用户id',`username` varchar(100) DEFAULT NULL,`password` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
 
 //查看tables
 show tables;
 
 // 插入一项
 INSERT INTO t_user VALUES(1,"admin","123456");

//查看表内容
 SELECT * FROM t_user;

在这里插入图片描述

2,idea中在pom.xml中加入依赖:

		<!-- 添加mysql依赖 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.30</version>
			<scope>runtime</scope>
			<!-- MySQL5.x时,请使用5.x的连接器(cmd执行mysql -V确定)
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.45</version>
			-->
		</dependency>

		<!-- 添加jdbc依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>

		<!-- 添加junit依赖 -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.13.2</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.20</version>
		</dependency>

3,在src/main/resources/application.properties中配置数据源信息

spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# mysql5: spring.datasource.driver-class-name=com.mysql.jdbc.Driver 

配置idea连接查看数据库:
(1)View-> Tool Windows -> Database进入:
在这里插入图片描述
如果社区版没有Database,搜索 file-settings-plugins,安装Database Navigator也是一样的。通过View-> Tool Windows -> DB Browser进入

(2)在Database视图中点击“+”号创建Mysql:
在这里插入图片描述
(3)选择要连接的数据库,Test Connection测试:
在这里插入图片描述
(4)打开可以看到对应的表项:
在这里插入图片描述

3,使用JdbcTemplate修改MySQL数据库:

新建实体类 如下:

User类:

package com.example.demospringboot;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class User {
    private int id;
    private String username;
    private String password;
}

UserService类:

package com.example.demospringboot;

import java.util.List;

public interface UserService {

    /**
     * 新增一个用户
     *
     * @param id
     * @param username
     * @param password
     */
    public void addUser(int id, String username, String password);

    /**
     * 删除一个用户
     * @param id
     */
    public void delUser(int id);

    /**
     * 根据id查询用户
     *
     * @param id
     * @return
     */
    List<User> getUserById(int id);

    /**
     * 查询全部用户
     * @return
     */
    public List<User> getUserAll();

    /**
     * 删除所有用户
     */
    public int delUserAll();
}

UserServiceImpl类

JDBCTemplate 提供3个操作数据的方法:
execute 直接执行 sql 语句
update 进行新增、修改、删除操作
query 查询操作

package com.example.demospringboot;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {
    private JdbcTemplate jdbcTemplate;

    public UserServiceImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void addUser(int id, String username, String password) {
        String sql = "INSERT INTO t_user VALUES(?,?,?)";
        jdbcTemplate.update(sql, id, username, password);
        System.out.println(id + " " + username + " " + password);
    }

    @Override
    public void delUser(int id) {
        String sql = "delete from t_user where id=?";
        jdbcTemplate.update(sql, id);
    }

    @Override
    public List<User> getUserById(int id) {
        String sql = "select ID, USERNAME, PASSWORD from t_user where id = ?";
        List<User> users = jdbcTemplate.query(sql, (resultSet, i) -> {
            User user = new User();
            user.setId(resultSet.getInt("ID"));
            user.setUsername(resultSet.getString("USERNAME"));
            user.setPassword(resultSet.getString("PASSWORD"));
            return user;
        }, id);
        return users;
    }

    @Override
    public List<User> getUserAll() {
        String sql = "select * from t_user";
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper(User.class));
    }

    @Override
    public int delUserAll() {
        return jdbcTemplate.update("delete from t_user");
    }
}

test用例:

package com.example.demospringboot;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemospringbootApplicationTests {

	@Autowired
	private UserService  userService;
	@Before
	public void setUp() {
		// 准备,清空user表
		userService.delUserAll();
	}

	@Test
	public void test() throws Exception {
		// 插入5个用户
		userService.addUser(1, "Tom", "1000");
		userService.addUser(2, "Mike", "1111");
		userService.addUser(3, "Didispace", "3000");
		userService.addUser(4, "Oscar", "2111");
		userService.addUser(5, "Linda", "1711");

		// 查询id为4的用户,判断名字是否为Oscar
		List<User> userList = userService.getUserById(4);
		Assert.assertEquals("Oscar", userList.get(0).getUsername());

		// 查数据库,应该有5个用户
		Assert.assertEquals(5, userService.getUserAll().size());

		// 删除两个用户
		userService.delUser(2);
		userService.delUser(3);

		// 查数据库,应该有3个用户
		Assert.assertEquals(3, userService.getUserAll().size());
	}
}

执行用例成功,查看数据库符合预期:

在这里插入图片描述

添加控制器 UserController :

package com.example.demospringboot;

import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/user")
@Log4j2
public class UserController {

    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    // 返回所有用户
    @GetMapping("/get")
    public List<User> getUserAll() {
        String sql = "SELECT id,username,password FROM t_user";
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
    }

    /**
     * 添加用户
     */
    @GetMapping("/add")
    public void addUser() {
        String sql = "INSERT INTO t_user VALUES(?,?,?)";
        int row = jdbcTemplate.update(sql,10, "Jacky", "123456");
        log.info("保存用户成功!保存个数: " + row);
    }
}

打开页面:
http://localhost:8080/user/add
http://localhost:8080/user/get
回显如下:

[{"id":1,"username":"Tom","password":"1000"},{"id":4,"username":"Oscar","password":"2111"},{"id":5,"username":"Linda","password":"1711"},{"id":10,"username":"Jacky","password":"123456"}]

总结

关于Spring所提供的JdbcTemplate的用法就是这些,Spring集成JdbcTemplate的方法也是比较简单的,整体就是先引入依赖,在配置数据库的连接,然后使用jdbcTemplate这个类就可以了,JdbcTemplate 类中就已经封装了大部分对于数据库的操作。

简单是简单,但是这个框架在企业级项目中应用是比较少的,一般用于对于操作数据库的要求不高的项目,因为他就是对jdbc的简单封装,所有的sql都是写入到代码中,维护性差,看起来也比较乱。后边我们会继续介绍比较主流的DAO层框架Mybatis和JPA的用法。希望本篇文章对大家有所帮助。

问题记录

1,编译运行报错如下:

Description: Parameter 0 of constructor in com.example.DemoSpringBoot.UserServiceImpl required a bean of type 'org.springframework.jdbc.core.JdbcTemplate' that could not be found

解决方案:
(1)推荐依赖spring-boot-starter-jdbc, 而不是spring-jdbc

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>

(2)引入Druid依赖,阿里巴巴所提供的数据源

        <!-- 引入Druid依赖,阿里巴巴所提供的数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.13</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>

appliction.properties:

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

2,当进行需要连接数据库的操作时,控制台会报下面这种红色警报:

Sat Jul 09 14:57:03 CST 2022 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.

解决方法 1 :在配置文件中的连接数据库的URL后面添加 useSSL=false

spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase?useSSL=false

方案二:可能是连接数据库的驱动版本问题(5.1.46),更换成其他版本即可。

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.46</version>
</dependency>

调整为

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.0.8</version>
</dependency>

3,报错:Web server failed to start. Port 8080 was already in use.

cmd命令如下:
在这里插入图片描述
参考:https://blog.didispace.com/spring-boot-learning-21-3-1/

二、使用MyBatis访问MySQL

添加依赖:

		<!-- 添加mybatis依赖 -->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.1</version>
		</dependency>
		<!-- 添加jdbc依赖
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		-->

properties跟之前一致:

spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
//mysql5: spring.datasource.driver-class-name=com.mysql.jdbc.Driver 

User类跟之前一致:

User类:

package com.example.demospringboot;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class User {
    private int id;
    private String username;
    private String password;
}

实现UserMapper接口:

package com.example.demospringboot;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
    @Select("SELECT * FROM t_user WHERE USERNAME= #{username}")
    User findByName(@Param("username") String name);

    @Insert("INSERT INTO t_user(ID, USERNAME, PASSWORD) VALUES(#{id}, #{username}, #{password})")
    int insert(@Param("id") Integer id, @Param("username") String username, @Param("password") String password);

    @Update("UPDATE t_user SET password=#{password} WHERE id=#{id}")
    void updatePassword(User user);

    @Delete("DELETE FROM t_user WHERE id =#{id}")
    void deleteById(int id);
}

@Mapper注解是MyBatis框架中的一个注解,它的作用是将一个Java接口标记为一个MyBatis的Mapper,使得这个接口可以被MyBatis框架自动扫描并生成对应的实现类。

具体来说,Mapper注解可以用来指定一个Java接口对应的Mapper XML文件的路径,以及Mapper接口中的方法与Mapper XML文件中的SQL语句的映射关系。如果多个接口都要变成实现类,那么需要在每个接口类上加上@Mapper注解,这样比较麻烦,我们通常会使用@MapperScan这个注解。

test用例:

package com.example.demospringboot;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.annotation.Rollback;

import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
@Rollback(false)
public class DemospringbootApplicationTests {

	@Autowired()
	private UserMapper userMapper;

	@Test
	public void testUserMapper() throws Exception {
		// 删除一条数据
		userMapper.deleteById(10);

		// insert一条数据,并select出来验证
		userMapper.insert(10, "Bob", "12345");
		User u = userMapper.findByName("Bob");
		Assert.assertEquals(10, u.getId());

		// update一条数据,并select出来验证
		u.setPassword("54321");
		userMapper.updatePassword(u);
		u = userMapper.findByName("Bob");
		Assert.assertEquals("54321", u.getPassword());
	}
}


在使用Mapper注解的情况下,我们不需要手动编写Mapper XML文件,而是可以直接在Java接口中定义SQL语句,MyBatis框架会自动将这些SQL语句与Mapper接口中的方法进行映射。但是实际项目中,我们还是会将sql语句和mapper分离,方便项目管理。改造如下:

UserMapper中只保留方法:

package com.example.demospringboot;

import org.apache.ibatis.annotations.Param;

public interface UserMapper {
    User findByName(@Param("username") String name);
    int insert(@Param("id") Integer id, @Param("username") String username, @Param("password") String password);
    void updatePassword(User user);
    void deleteById(int id);
}

将sql语句移入新增的resources\mybatis\UserMapper.xml文件,并在application.properties中配置路径:

mybatis.mapper-locations=classpath:mybatis/*xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- mapper标签要指定namespace属性,可看做包名-->
<mapper namespace="com.example.demospringboot.UserMapper">
    <select id="findByName" parameterType="string" resultType="com.example.demospringboot.User">
        SELECT * FROM t_user WHERE USERNAME= #{username}
    </select>
    <insert id="insert" parameterType="com.example.demospringboot.User">
        INSERT INTO t_user(ID, USERNAME, PASSWORD) VALUES(#{id}, #{username}, #{password})
    </insert>
    <delete id="deleteById" parameterType="int">
        DELETE FROM t_user WHERE id =#{id}
    </delete>
    <update id="updatePassword" parameterType="com.example.demospringboot.User">
        UPDATE t_user SET password=#{password} WHERE id=#{id}
    </update>
</mapper>

MyBatis映射文件Mapper XML文件标签:https://blog.csdn.net/zwx900102/article/details/108534736

主函数使用@MapperScan

package com.example.demospringboot;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan(value = {"com.example.demospringboot"})
public class DemospringbootApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemospringbootApplication.class, args);
	}
}

druid多数据源的配置使用

如下配置设置两个数据源:

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

spring.datasource.first.url=jdbc:mysql://localhost:3306/mydatabase1
spring.datasource.first.username=root
spring.datasource.first.password=1234

spring.datasource.second.url=jdbc:mysql://localhost:3306/mydatabase2
spring.datasource.second.username=root
spring.datasource.second.password=1234

#初始化连接大小
spring.datasource.druid.initial-size=0
#连接池最大使用连接数量
spring.datasource.druid.max-active=20
#连接池最小空闲
spring.datasource.druid.min-idle=0
#获取连接最大等待时间
spring.datasource.druid.max-wait=6000
spring.datasource.druid.validation-query=SELECT 1
#spring.datasource.druid.validation-query-timeout=6000
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.test-while-idle=true
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
#置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=25200000
#spring.datasource.druid.max-evictable-idle-time-millis=
#打开removeAbandoned功能,多少时间内必须关闭连接
spring.datasource.druid.removeAbandoned=true
#1800秒,也就是30分钟
spring.datasource.druid.remove-abandoned-timeout=1800
#<!-- 1800秒,也就是30分钟 -->
spring.datasource.druid.log-abandoned=true
spring.datasource.druid.filters=mergeStat

mybatis.mapper-locations=classpath:mybatis/*.xml

Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行数据库操作之前,设置使用的数据源,即可实现数据源的动态路由。它的抽象方法determineCurrentLookupKey() 决定使用哪个数据源。

1、DynamicDataSource继承AbstractRoutingDataSource,重写determineCurrentLookupKey方法:

package com.example.demospringboot;

import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.Map;

/**动态数据源
 * 扩展 Spring 的 AbstractRoutingDataSource 抽象类,重写 determineCurrentLookupKey 方法
 * determineCurrentLookupKey() 方法决定使用哪个数据源
 *
 */
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 决定使用哪个数据源之前需要把多个数据源的信息以及默认数据源信息配置好
     *
     * @param defaultTargetDataSource 默认数据源
     * @param targetDataSources       目标数据源
     */
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

数据源切换的处理类如下:

package com.example.demospringboot;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class DynamicDataSourceContextHolder {

    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源变量
     * @param dataSourceType
     */
    public static void setDataSourceType(String dataSourceType){
        log.info("切换到{}数据源", dataSourceType);
        CONTEXT_HOLDER.set(dataSourceType);
    }

    /**
     * 获取数据源变量
     * @return
     */
    public static String getDataSourceType(){
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

2,编写枚举类,用于指定数据源

package com.example.demospringboot;

public enum DataSourceType {
    MASTER,
    TARGET
}

多数据源的配置启动类,注入数据库配置信息

package com.example.demospringboot;

import com.example.demospringboot.DataSourceType;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

import static com.example.demospringboot.DataSourceType.*;

/**
 * 配置多数据源
 */
@Configuration
public class DynamicDataSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.first")
    public DataSource firstDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.second")
    public DataSource secondDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    @Primary // 自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常
    public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER.name(), firstDataSource);
        targetDataSources.put(DataSourceType.TARGET.name(), secondDataSource);
        return new DynamicDataSource(firstDataSource, targetDataSources);
    }

}


3,自定义注解进行数据源切换:

package com.example.demospringboot;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    /**
     * 切换数据源名称
     */
    DataSourceType value() default DataSourceType.MASTER;
}

AOP拦截类的实现:

package com.example.demospringboot;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Order(1)
@Component
public class DataSourceAspect {
    @Pointcut("@annotation(com.example.demospringboot.DataSource)")
    public void dsPointCut() {

    }

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DataSource dataSource = method.getAnnotation(DataSource.class);
        if (dataSource != null) {
            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
        }
        try {
            return point.proceed();
        } finally {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }
}

4,测试类的编写验证

package com.example.demospringboot;

import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.annotation.Rollback;


@RunWith(SpringRunner.class)
@SpringBootTest
//@Transactional
@Rollback(false)
@Slf4j
public class DemospringbootApplicationTests {

	@Autowired()
	private UserMapper userMapper;

	@Autowired()
	DynamicDataSource dynamicDataSource;

	@Test
	public void testUserMapper() throws Exception {

		// delete、insert对应方法默认使用@DataSource(DataSourceType.TARGET) 注解,insert到了target
		userMapper.deleteById(1);
		userMapper.insert(1, "Bob", "12345");

		// 设置数据源为MASTER,返回空
		DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
		User u = userMapper.findByName("Bob");
		Assert.assertNull(u);

		// 设置数据源为TARGET,返回对应数据
		DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.TARGET.name());
		u = userMapper.findByName("Bob");
		Assert.assertEquals(1, u.getId());
	}
}



UserMapper 如下:

package com.example.demospringboot;

import org.apache.ibatis.annotations.Param;

public interface UserMapper {
    User findByName(@Param("username") String name);

    @DataSource(DataSourceType.TARGET)
    int insert(@Param("id") Integer id, @Param("username") String username, @Param("password") String password);
    @DataSource(DataSourceType.TARGET)
    void deleteById(int id);
}

可以看到切换数据源的打印,插入的数据源也符合预期。

注:
Mybatis切换数据源未生效,可能原因如下:
spring开启事务后会维护一个ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。检查整个调用链路涉及的类的方法和类本身还有继承的抽象类上是否有@Transactional注解。

既然用了Druid,那么下面就来再进一步做一些配置,来启用Druid的监控。参考:配置Druid监控

问题记录

第一次用例执行通过,但是打开数据库发现并没有没有真正写入,原来是发生了回滚,原因跟@Transactional注解有关:

@Transactional注解是Spring框架中用于管理事务的注解。它可以被应用在类或方法上,用于指定一个方法或类应该被包含在一个事务中。
当@Transactional注解被应用在方法上时,它会将该方法的执行过程包装在一个事务中。如果方法执行成功,则事务会被提交,如果方法执行失败,则事务会被回滚。

但是,Junit单元测试时,如果在测试方法中打事务注解@Transactional,默认会按照@Rollback(true)来进行处理,即使在没加注解@Rollback,也会对事务回滚,解决办法如下:

方法1:测试方法上在注解@Transactional的基础上,加注解@Rollback(value = false)
方法2:测试方法上在注解@Transactional的基础上,加注解@Commit

我们在application.properties中配置

mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

重新执行test,可以看到比之前多了committing动作:

Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1b560eb0]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1e40fbb3]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1b560eb0]

参考:
https://blog.didispace.com/spring-boot-learning-21-3-5/
https://blog.csdn.net/javakaka666/article/details/122997933

三、使用Spring Data JPA访问MySQL

在之前,我们使用JDBC或是Mybatis来操作数据,通过直接编写对应的SQL语句来实现数据访问,然而当我们有一定的开发经验之后,不难发现,在实际开发过程中,对数据库的操作大多可以归结为:“增删改查”。为了解决这些大量枯燥的数据操作语句,诞生了非常多的优秀框架,比如:JPA规范的框架一般最常用的Hibernate。

JPA(Java Persistence API)和JDBC类似,也是官方定义的一组接口,但是它相比传统的JDBC,它是为了实现ORM而生的,即Object-Relationl Mapping,它的作用是在关系型数据库和对象之间形成一个映射,这样,我们在具体的操作数据库的时候,就不需要再去和复杂的SQL语句打交道,只要像平时操作对象一样操作它就可以了。

而实现JPA规范的框架一般最常用的就是Hibernate,它是一个重量级框架,学习难度相比Mybatis也更高一些,而SpringDataJPA也是采用Hibernate框架作为底层实现,并对其加以封装。

1,在pom.xml中添加相关依赖,加入以下内容:

		<!-- 添加jpa依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>javax.persistence</groupId>
			<artifactId>persistence-api</artifactId>
			<version>1.0.2</version>
		</dependency>

2,在application.properties中配置

spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# mysql5: spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.jpa.properties.hibernate.hbm2ddl.auto=create-drop

spring.jpa.properties.hibernate.hbm2ddl.auto是hibernate的配置属性,其主要作用是:自动创建、更新、验证数据库表结构。该参数的几种配置如下:

  • create:每次加载hibernate时都会删除上一次的生成的表,然后根据你的model类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。
  • create-drop:每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。
  • update:最常用的属性,第一次加载hibernate时根据model类会自动建立起表的结构(前提是先建立好数据库),以后加载hibernate时根据model类自动更新表结构,即使表结构改变了但表中的行仍然存在不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等应用第一次运行起来后才会。
  • validate:每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。

3,创建一个User实体,通过ORM框架其会被映射到数据库表中,由于配置了hibernate.hbm2ddl.auto,在应用启动的时候框架会自动去数据库中创建对应的表。

package com.example.demospringboot;

import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
@Data
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue
    private Integer id;
    private String username;
    private String password;

    public User(Integer id, String username, String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }
}

@Entity注解标识了User类是一个持久化的实体
@Data和@NoArgsConstructor是Lombok中的注解。用来自动生成各参数的Set、Get函数以及不带参数的构造函数。如果您对Lombok还不了解,可以看看这篇文章:Java开发神器Lombok的使用与原理
@Id和@GeneratedValue用来标识User对应对应数据库表中的主键

4,创建数据访问接口
下面针对User实体创建对应的UserRepository 接口实现对该实体的数据访问,如下代码:
在Spring Data JPA中,只需要编写类似这样的接口就可实现数据访问。不再像我们以往编写了接口时候还需要自己编写接口实现类,直接减少了我们的文件清单。

package com.example.demospringboot;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface UserRepository extends JpaRepository<User, Integer> {

    User findByUsername(String username);

	User findByUsernameAndPassword(String username, String password);

    @Query("from User u where u.username=:username")
    User findUser(@Param("username") String username);

}

下面对UserRepository做一些解释,该接口继承自JpaRepository,通过查看JpaRepository接口的API文档,可以看到该接口本身已经实现了创建(save)、删除(delete)、查询(findAll、findOne)等基本操作的函数,因此对于这些基础操作的数据访问就不需要开发者再自己定义。

在我们实际开发中,JpaRepository接口定义的接口往往还不够或者性能不够优化,我们需要进一步实现更复杂一些的查询或操作。由于本文重点在Spring Boot中整合spring-data-jpa,在这里先抛砖引玉简单介绍一下spring-data-jpa中让我们兴奋的功能,后续再单独开篇讲一下spring-data-jpa中的常见使用。

在上例中,我们可以看到下面两个函数:

    User findByUsername(String username);
    User findByUsernameAndPassword(String username, String password);

它们分别实现了按username查询User实体和按username和id查询User实体,可以看到我们这里没有任何类SQL语句就完成了两个条件查询方法。这就是Spring-data-jpa的一大特性:通过解析方法名创建查询。例如findByUsername(String username)方法会自动生成查询语句"SELECT * FROM user WHERE username = ?"

除了通过解析方法名来创建查询外,它也提供通过使用@Query 注解来创建查询,您只需要编写JPQL语句,并通过类似“:username”来映射@Param指定的参数,就像例子中的findUser函数一样。

5,单元测试用例:

package com.example.demospringboot;

import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemospringbootApplicationTests {

	@Autowired
	private UserRepository userRepository;

	@Test
	public void test() throws Exception {
		// 创建5条记录
		userRepository.save(new User(10,"AAA","1000"));
		userRepository.save(new User(20, "BBB", "2000"));
		userRepository.save(new User(30,"CCC", "3000"));
		userRepository.save(new User(40,"DDD", "4000"));
		userRepository.save(new User(50, "EEE", "5000"));

		// 测试findAll, 查询所有记录
		Assert.assertEquals(5, userRepository.findAll().size());

		// 测试findByName, 查询姓名为EEE的User
		Assert.assertEquals("5000", userRepository.findByUsername("EEE").getPassword());

		// 测试findUser, 查询姓名为EEE的User
		Assert.assertEquals("5000", userRepository.findUser("EEE").getPassword());

   		// 测试findByUserNameAndPassword, 查询姓名为EEE并且密码为5000的User
        Assert.assertEquals("EEE", userRepository.findByUsernameAndPassword("EEE", "5000").getUsername());

		// 测试删除姓名为AAA的User
		userRepository.delete(userRepository.findByUsername("AAA"));

		// 测试findAll, 查询所有记录, 验证上面的删除是否成功
		Assert.assertEquals(4, userRepository.findAll().size());

	}
}

执行后可以看到mysql多了名为user的table,并且其中添加了 4 rows:
在这里插入图片描述

参考:
https://blog.didispace.com/spring-boot-learning-21-3-4/
https://blog.csdn.net/hc1285653662/article/details/124642856
https://blog.csdn.net/weixin_48583915/article/details/130631197
https://www.cnblogs.com/hungryquiter/p/17656137.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值