本文介绍: 通过this调用一个service中的方法this是指service实现对象本身,不是代理对象,就相当于方法中的代码粘到了大方法里面,相当于还是一个方法t_user数据不会变化,因为执行这条sql时候报错了嘛,但是t_book这个表里面库存减一了,也就是“买书步骤”中的第三报错了,但是第二步依旧没回滚。若B方法执行过程中,只要有抛出异常,B事务方法回滚,A不会回滚,即不影响A中的其他事务执行(如第二次买书失败影响一次买书成成功)mybatis默认开启驼峰命名规则

JDBC

1、SQL准备

DROP TABLE IF EXISTS `t_book`;

CREATE TABLE `t_book` (
  `book_id` int(11) NOT NULL,
  `book_name` varchar(255) DEFAULT NULL,
  `price` int(11) DEFAULT NULL,
  `stock` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

/*Data for the table `t_book` */

insert  into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'仙逆',30,100),(2,'诛仙',20,100);

/*Table structure for table `t_user` */

DROP TABLE IF EXISTS `t_user`;

CREATE TABLE `t_user` (
  `user_id` int(11) NOT NULL,
  `username` varchar(100) DEFAULT NULL,
  `balance` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

/*Data for the table `t_user` */

insert  into `t_user`(`user_id`,`username`,`balance`) values (1,'zhangsan',500);

java Bean

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Integer userId;
    private String username;
    private Integer balance;
}

1


2、数据库驱动

JDBC(Java DataBase Connectivity),即Java数据库连接。简而言之,就是通过Java语言操作数据库

JDBCsun公司提供一套用于数据库操作接口.

java程序员需要面向这套接口编程即可不同数据库厂商MySQLOracleDB2、SQLServer 等),需要实现类去实现这套接口,再把这些实现类打包数据驱动jar包),并提供数据驱动jar包给我们使用。   

驱动:就是一个jar包,里面包含了JDBC的实现类

想要通过JDBC连接操作Mysql数据库我们需要下载一个Mysql数据库驱动jar包。所以我们下面都能看到引入了这个依赖

<!-- mysql驱动 --&gt;
<dependency&gt;
    <groupId&gt;mysql</groupId&gt;
    <artifactId&gt;mysql-connector-java</artifactId>
</dependency>

JDBC API主要位于JDK中的java.sql包中(之后扩展内容位于javax.sql包中),主要包括:


3、JDBC编程六步

一步注册驱动    作用:告诉Java程序,即将要连接的是哪个数据库

通过DriverManager.registerDriver(driver)注册驱动

String className = "com.mysql.cj.jdbc.Driver";
//1. 注册驱动
Class clazz = Class.forName(className);
Driver driver = (Driver) clazz.newInstance();
DriverManager.registerDriver(driver);

第二步:获取连接    表示JVM的进程数据进程之间的通道打开了,这属于进程之间的通信使用完之后一定要关闭通道

//2. 获取连接
String url = "jdbc:mysql://127.0.0.1:3306/spring-boot-demo?serverTimezone=UTC";
String user = "root";
String password = "root";
Connection conn = DriverManager.getConnection(url, user, password);

第三步:获取数据操作对象    专门执行sql语句对象

//3.获取数据库操作对象
Statement stmt = conn.createStatement();

第四步:执行SQL语句    DQL DML…

 //4.执行查询 SQL语句返回结果集
 String username = "zhangsan";
 String sql = "select * from t_user where  username= '" + username + "'";
 ResultSet rs = stmt.executeQuery(sql);

第五步:处理查询结果集    只有当第四步执行的是select语句时候,才有这第五步处理查询结果集。如果不是的话,可以直接释放资源

根据查询结果集中字段下标获取

//5.通过索引遍历读取结果while (rs.next()) {
    int userId = rs.getInt(1);
    String name = rs.getString(2);
    String balance = rs.getString(3);
    System.out.println("userId:" + userId + " 姓名:" + name + " 余额:" + balance);
}

六步:释放资源    使用资源之后一定要关闭资源。Java和数据属于进程间的通信开启之后一定要关闭

//5.资源的释放,讲道理要写到finally语句块中
rs.close();
stmt.close();
conn.close();

完整代码

@SpringBootTest
public class TestA {
    @Test
    public void test2() throws Exception {
        String className = "com.mysql.cj.jdbc.Driver";

        //1. 注册驱动
        Class clazz = Class.forName(className);
        Driver driver = (Driver) clazz.newInstance();
        DriverManager.registerDriver(driver);

        //2. 获取连接
        String url = "jdbc:mysql://127.0.0.1:3306/spring-boot-demo?serverTimezone=UTC";
        String user = "root";
        String password = "root";
        Connection conn = DriverManager.getConnection(url, user, password);

        //3.获取数据库操作对象
        Statement stmt = conn.createStatement();

        //4.执行查询 SQL语句,返回结果集
        String username = "zhangsan";
        String sql = "select * from t_user where  username= '" + username + "'";
        ResultSet rs = stmt.executeQuery(sql);

        //5.通过索引遍历读取结果集
        while (rs.next()) {
            int userId = rs.getInt(1);
            String name = rs.getString(2);
            String balance = rs.getString(3);
            System.out.println("userId:" + userId + " 姓名:" + name + " 余额:" + balance);

        }
        //5.资源的释放,讲道理要写到finally语句块中
        rs.close();
        stmt.close();
        conn.close();
    }
}

4、PreparedStatement

SQL注入实例

定义SQL语句框架时候使用PreparedStatement数据库操作对象,这个是预编译对象,先将SQL语句框架进行了编译,然后参数?动态赋值

Statement 和 PreparedStatement 对比
PreparedStatement可以防止 SQL 注入,执行效率高
SQL语句对于Statement来说是:编译一次执行一次
SQL语句对于PreparedStatement来说,是编译一次执行N次
原因数据管理系统(DBMS)厂商实现了JDBC接口,DBMS将编译后的SQL语句保存在DBMS中,由于DBMS中有很多编译好的SQL语句,这时通过同一个PreparedStatement对象进行赋值,便会找到其对应的PreparedStatement对象,从而实现其参数赋值,即:一次编译多次执行。
PreparedStatement是类型安全的,编译期可以检查传入参数类型


5、JDBC事务

开启事务(设置手动提交事务):conn.setAutoCommit(false);

事务提交:conn.commit();

事务回滚:conn.rollback();

Connection con = null;
        try {
            con = getConnection();
            con.setAutoCommit(false);
            /*
             * do what you want here.
             */
            con.commit();
        } catch (Throwable e) {
            if (con != null) {
                try {
                    con.rollback();// 设定setAutoCommit(false)若没有catch中进行Connection的rollBack操作,操作的表就会被锁住,造成数据死锁。虽然在执行con.close()的时候会释放锁,但若应用服务器使用了数据库连接池,连接不会被断开,从而不会放锁
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            }

            throw new RuntimeException(e);
        } finally {
            if (con != null) {
                try {
                    con.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

SpringBoot整合JdbcTemplate

1、启动依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

2、yml配置数据源

数据源DataSource有什么作用?   通过DataSource可以获取数据库连接Connection

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&amp;characterEncoding=UTF-8&amp;useSSL=false&amp;autoReconnect=true&amp;failOverReadOnly=false&amp;serverTimezone=GMT%2B8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

3、启动时数据库初始化

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&amp;characterEncoding=UTF-8&amp;useSSL=false&amp;autoReconnect=true&amp;failOverReadOnly=false&amp;serverTimezone=GMT%2B8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    initialization-mode: always
    continue-on-error: true
    schema:
    - "classpath:db/schema.sql"
    data:
    - "classpath:db/data.sql"

默认加载schema.sql与data.sql。

spring.datasource.schme

schema为表初始化语句,

spring.datasource.data

data为数据初始化

spring.datasource.initialization-mod初始化模式springboot2.0),其中有三个值,always为始终执行初始化embedded初始化内存数据库(默认值),如h2等,never为不执行初始化

spring.datasource.continue-on-error: false   遇到语句错误是否继续,若已经执行过某些语句,再执行可能报错,可以忽略,不会影响程序启动


4、JdbcTemplate的使用

定义一个UserDao操作 t_uer 表

@Repository
public class UserDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;


    /**
     * 根据用户名获取用户
     *
     * @param uname 用户名
     * @return 对应用户
     */
    public User selectByName(String uname) {
        String sql = "select * from t_user where username = ?";
        /**
         * sql是要执行的 SQL 查询。
         * rowMapper是一个回调,它将每行映射一个对象
         * args 是要绑定到查询的参数。
         */
        User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), uname);
        return user;
    }
}

一条数据为一个实体类对象

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

    @Autowired
    UserDao userDao;

    @Test
    public void test1() {
        User user = userDao.selectByName("zhangsan");
        System.out.println(user);
    }
}

结果

JdbcTemplate封装了许多SQL操作,具体可查阅官方文档JdbcTemplate (Spring Framework 6.0.12 API)


SpringBoot整合mybatis

1、依赖

不同版本的Spring Boot和MyBatis版本对应不一样,具体可查看官方文档mybatis-spring-boot-autoconfigure – Introduction

mybatisspringbootstarter

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

注意:有2种方式可以指定重新指定mysql版本


2、yml配置

server:
  port: 8089  #tomcat端口号
logging:
  level:
     com.atguigu.dao: debug # 配置日志
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://127.0.0.1:3306/springboot?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  type-aliases-package: com.atguigu.pojo

指定mybati sql映射文件位置

如数据库里这个字段是这样的user_id,实体类里是这样的userId。mybatis默认开启驼峰命名规则。即默认情况下无法匹配

mybatis默认不开启驼峰命名规则。即默认情况下无法匹配。

不用去指定全局配置文件mybatis-config.xml; 而使用mybatis.configuration去代替它。


3、@Mapper注解

mybatis接口要创建代理对象,原来是通过sqlSession.getMapper(UserMapper.class),现在加上@Mapper才行

告诉mybatis这是一个Mapper接口,来操作数据库

@Mapper
public interface AccountMapper{
 
  public Account getAcct(Long id);
 
}

当然,每一个mapper接口都加的话太麻烦了,直接在启动类里做处理就好了

 @MapperScan(basePackage=“”),扫描包,提供代理对象

@SpringBootApplication
@MapperScan(basePackages = "com.atguigu.dao")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

 在测试类 MybatisTest【一定要被启动类覆盖

@RunWith(SpringJUnit4ClassRunner.class) //指定Junit核心运行类
@SpringBootTest //自动提供IOC容器
public class MybatisTest {
    @Autowired
    private UserMapper userMapper ;

    @Test
    public void findAll() {
        List<User> users = userMapper.selectAll();
        System.out.println(users);
    }
}

测试结果:因为没有重写toString


4、整合mybatis-plus

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.1.0</version>
</dependency>

配置文件

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&amp;characterEncoding=UTF-8&amp;useSSL=false&amp;autoReconnect=true&amp;failOverReadOnly=false&amp;serverTimezone=GMT%2B8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.zaxxer.hikari.HikariDataSource
    initialization-mode: always
    continue-on-error: true
    schema:
    - "classpath:db/schema.sql"
    data:
    - "classpath:db/data.sql"
    hikari:
      minimum-idle: 5
      connection-test-query: SELECT 1 FROM DUAL
      maximum-pool-size: 20
      auto-commit: true
      idle-timeout: 30000
      pool-name: SpringBootDemoHikariCP
      max-lifetime: 60000
      connection-timeout: 30000
logging:
  level:
    com.xkcoding: debug
    com.xkcoding.orm.mybatis.plus.mapper: trace
mybatis-plus:
  mapper-locations: classpath:mappers/*.xml
  #实体扫描多个package逗号或者分号分隔
  typeAliasesPackage: com.xkcoding.orm.mybatis.plus.entity
  global-config:
    # 数据库相关配置
    db-config:
      #主键类型  AUTO:"数据库ID自增", INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
      id-type: auto
      #字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
      field-strategy: not_empty
      #驼峰下划线转换
      table-underline: true
      #是否开启大写命名,默认不开启
      #capital-mode: true
      #逻辑删除配置
      #logic-delete-value: 1
      #logic-not-delete-value: 0
      db-type: mysql
    #刷新mapper 调试神器
    refresh: true
  # 原生配置
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: true

mybatis-plus配置

@Configuration
@MapperScan(basePackages = {"com.atguigu.mapper"})
@EnableTransactionManagement
public class MybatisPlusConfig {
    /**
     * 性能分析拦截器,不建议生产使用
     */
    @Bean
    public PerformanceInterceptor performanceInterceptor() {
        return new PerformanceInterceptor();
    }

    /**
     * 分页插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
}

SpringBoot整合JPA

1、依赖

spring  data JPA 对  hibernate做了封装,底是Hibernate

添加Spring Data JPA的起步依赖

        <!-- springBoot JPA的起步依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
 
        <!-- MySQL连接驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

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

2、yml配置

application.yml配置数据库和jpa相关属性

日志级别
fatal error warn info debug   级别越低,信息越多

logging:
  level:
    com.atguigu.dao: debug # 配置日志
server:
  port: 8089  #tomcat端口号
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    database: mysql # 数据库类型
    show-sql: true
    generate-ddl: true #生成结构
    hibernate:
      ddl-auto: update
      naming_strategy: org.hibernate.cfg.ImprovedNamingStrategy  #表名字字段名字的命名策略

spring.jpa.hibernate.ddl-auto属性有以下几种参数:

create:每次加载hibernate会自动创建表,以后启动会覆盖之前的表,所以这个值基本不用,会导致的数据的丢失
createdrop : 每次加载hibernate时根据model生成表,但是sessionFactory一关闭,表就自动删除,下一次启动会重新创建。
update加载hibernate时根据实体类model创建数据库表,这是表名的依据是@Entity注解的值或者@Table注解的值,sessionFactory关闭表不会删除,且下一次启动会根据实体model更新结构或者有新的实体类会创建新的表。
validate:启动时验证表的结构,不会创建表。
none:启动时不做任何操作。


3、User实体类

@Id   标明主键
@GeneratedValue  设置主键自增 

很重要的一个类,会根据这个类在数据库中生成表。

import lombok.Data;
import javax.persistence.*;

@Entity
@Table(name = "jpa_user")
@Data
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "username")
    private String username;
    @Column(name = "password")
    private String password;
    @Column(name = "age")
    private Integer age;
}

项目跑起来,让自动创建表jpa_user


4、UserDao

JpaRepository接口同时拥有了基本CRUD功能以及分页功能。

我们需要定义自己Repository接口的时候我们可以直接继承JpaRepository,从而获得SpringBoot Data JPA为我们内置的多种基本数据操作方法

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface JpaUserDao extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

内置方法:

<S extends T> S save(S entity); 保存一个实体

Optional<T> findById(ID id); //根据id查询对应的实体

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

    @Autowired
    JpaUserDao jpaUserDao;

    @Test
    public void test1() {
        User user = new User();
        user.setUsername("zhangsan");
        user.setPassword("123456");
        user.setAge(20);
        User user1 = jpaUserDao.save(user);
        System.out.println("保存成功后返回的userId=" + user1.getId());

        Optional<User> user2 = jpaUserDao.findById(user1.getId());
        System.out.println("查询得到user:" + user2.get());
    }
}


SpringBoot事务管理

1、准备工作

使用mybatis-plus

实体

@Data
public class Book {
    private Integer bookId;
    private String bookName;
    private Integer price;
    private Integer stock;//库存
}
@Data
public class User {
    private Integer userId;
    private String username;
    private Integer balance;
}

mapper

@Component
public interface UserMapper extends BaseMapper<User> {
}
@Component
public interface BookMapper extends BaseMapper<Book> {
}

service接口

public interface UserService extends IService<User> {
    //更新用户余额
    boolean updateBalance(Integer bookId, Integer userId);
}
public interface BookService extends IService<Book> {
    void buyBook(Integer bookId, Integer userId);
}

service接口实现类

@Service
public class BookServiceImpl extends ServiceImpl<BookMapper, Book> implements BookService {
    @Override
    public void buyBook(Integer bookId, Integer userId) {
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId, price);
    }
}

1

public interface UserService extends IService<User> {
    //更新用户余额
    boolean updateBalance(Integer bookId, Integer userId);
}


2、开启事务的注解驱动

当然在之后的springboot项目中,就不这么麻烦了,直接在启动类上加

@EnableTransactionManagement

开启后通过注解@Transactional标识的方法或标识类中所有的方法,都会被事务管理器处理事务

@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Transactional {

}

3、场景一:买书

果实际的业务中,需要一条数据同时存放到两张表中, 并且要求两张表中的数据同步,那么此时就需要使用事务管理机制,保证数据同步。如果出现错误情况,比如表一插入数据成功,表二插入数据失败,那么就回滚,终止数据持久化操作。

模拟场景:买书

模拟买书三步骤,需要进行事务管理

  1. 查询图书价格(t_book)
  2. 更新图书库存(t_book)
  3. 更新用户余额(t_user)

没有事务管理的情况

BookServiceImpl中的买书方法,注意此时buyBook()上没有加@Transactional。

public interface BookService {
    void buyBook(Integer bookId, Integer userId);
}
   @Override 
   public void buyBook(Integer bookId, Integer userId) {
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId, price);
    }

测试如下代码

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class BookControllerTest {
    @Autowired
    private BookController bookController;

    @Test
    public void test1(){
        bookController.buyBook(1,1);
    }
}

报错是情理之中,

因为:从下面2表数据(测试方法执行前)可看得出admin这个用户余额只有80,它买一本价格为100的《斗破苍穹》就会报错,因为表结构中余额是无符号整数

那么由于我们没有事务管理,此时表中的数据就不对劲了

t_user中数据不会变化,因为执行这条sql的时候报错了嘛,但是t_book这个表里面的库存减一了,也就是“买书三步骤”中的第三步报错了,但是第二步依旧没回滚。


加入事务管理解决

以 @Transactional 注解为植入点的切点,这样才能知道@Transactional注解标注的方法需要被代理

如何避免上面那种错呢,很简单就是加一注解@Transactional,搞定!

    @Override
    @Transactional
    public void buyBook(Integer bookId, Integer userId) {
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId, price);
    }

 我们把t_book中的库存,改回为100,再执行一遍测试代码

bookController.buyBook(1,1);

报错肯定是要报错的,

 但是这时候,t_book表中的库存并没有改,说明事务回滚了!nice

在实际开发中好多都是业务逻辑出错导致的回滚:如库存不够和余额不够。

就拿卖书的例子来说,要是我们没有把金额设置为无符号整数,对数据库来说,整数也可以为负,从逻辑来说,金额不能为负,所以解决方案有2种:

1、从数据库方面来解决:无符号整数unsigned
2、从Java代码方面来解决:没有异常给造一个异常

4、事务属性

@Transactional就是用来定位连接点,可以标识类或方法上。

@Transactional注解:通过该注解所标识的方法或类中所有的方法会被事务管理器处理事务 

​​​​​​​@Transactional原理aop调用目标方法进行了扩展
1.方法调用前开启事务 
2.方法调用时它对方法进行了try...catch,如果进入catch中则回滚事务
3.如果try的最后一行代码能执行到则提交事务

@Transactional 事务的属性

事务的属性有 只读超时时间、回滚策略、隔离级别、传播行为

    @Transactional(
            readOnly = false,
            timeout = 30,
            //noRollbackForClassName = "java.lang.ArithmeticException",
            noRollbackFor = ArithmeticException.class,
            isolation = Isolation.DEFAULT,
            propagation = Propagation.REQUIRES_NEW
    )

但是不要慌,事务的这些属性都有默认值

1、事务的只读

通过@Transactional注解的readOnly属性设置默认值为false

当前的事务设置只读,则会在数据库层面去优化该操作,比如加锁

注意:只有事务中只有查询功能时,才可以设置事务的只读,即readOnly = true
设置只读的事务中有任何的增删改操作,则抛出异常:
java.sql.SQLException: Connection is readonly

2、事务的超时时间

通过@Transactional注解的timeout属性设置默认值为-1,表示往死里等(单位秒)

因为事务针对连接来操作,如果连接一直处于阻塞,就会无限制等待下去

注意:若当前事务设置了事务的超时时间,但是在指定时间内没有执行完毕则抛出事务超时异常TransactionTimedOutException,并将事务强制回滚

我们可以让线程阻塞! TimeUnit.SECONDS.sleep(5);

    @Override
    @Transactional(
            timeout = 3
    )
    public void buyBook(Integer bookId, Integer userId) {
        try {
            //线程暂停5秒,可读性强
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId, price);
    }

运行结果,不出所料


3、事务的回滚策略

声明式事务 默认只针对运行时异常回滚,编译时异常不回滚。

通过@Transactional注解的rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName设置

rollbackFor和rollbackForClassName指定的异常必须回滚 ,一般不设置
noRollbackFor和noRollbackForClassName指定的异常不用回滚

rollbackFor和noRollbackFor通过异常的class对象设置
rollbackForClassName和noRollbackForClassName通过异常的全类名设置​​​​​​​

rollbackFo举例

rollbackFor 指定回滚的情况。spring默认是回滚RuntimeException或这error才回滚。当然自定义的RuntimeException异常类也是可以的。

​ 如果希望spring能够回滚别类型的异常,那就需要使用rollbackFor去指定(当然如果是指定异常的子类,也同样会回滚

@Transactional(rollbackFor=Exception.class)

———————————-

noRollbackFor举例

用80去买价格为50一本的《斗罗大陆》,是可以正常买成功的;

但是我们程序最后加了一个数学运算异常,那么按道理来说,会回滚事务。

但是加了noRollbackFor之后,再发生数学运算异常的时候,事务就不会回滚了。。

    @Override
    @Transactional(noRollbackFor = ArithmeticException.class)
    public void buyBook(Integer bookId, Integer userId) {
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId, price);
        System.out.println(1/0);
    }

结果就是,发生了数学运算异常,但是事务没有回滚,数据库数据依旧发生了改变


4、事务的隔离级别

通过@Transactional注解的isolation设置事务的隔离级别,一般使用数据库默认的隔离级别.

隔离级别越高,数据一致性就越好,但并发性越弱。

isolation = Isolation.DEFAULT;//表示使用数据库默认的隔离级别
isolation = Isolation.READ_UNCOMMITTED;//表示读未提交
isolation = Isolation.READ_COMMITTED;//表示读已提交
isolation = Isolation.REPEATABLE_READ;//表示重复读
isolation = Isolation.SERIALIZABLE;//表示串行化

5、事务的传播行为

事务的传播:当A事务方法调用了B事务方法,A方法在执行时,就会将其本身的事务传播给B方法
B方法执行的过程中,使用A传播过来的事务,也可以使用其本身即B的事务

更详细的可以参考: 
22-05-13 西安 jdbc(03) 事务的ACID属性、并发问题、隔离级别;事务传播行为、本地事务_£小羽毛的博客-CSDN博客_jdbc事务 并发

场景模拟:结账

创建CheckoutServiceImpl,这里我们有个很重要的事情,就是结账:checkout()

它是一个事务方法,它调用了BookServiceImpl的事务方法buyBook()

场景模拟:结账
@Service
public class CheckoutServiceImpl implements CheckoutService {
    @Autowired
    private BookService bookService;

    @Override
    @Transactional
    public void checkout(int[] bookIds, int userId) {
        for (int bookId : bookIds) {
            bookService.buyBook(bookId, userId);
        }
    }
}

在默认情况下,即不改变事务的默认属性:

测试:修改用户的余额为120,目的为了让用户可以买第一本书,但是在买第二本的时候,会因为余额不够而报出异常。

bookController.checkout(new int[]{1,2},1);

此时,观察数据库结果发现俩张表数据都无变化,当“有一本书不能买,那就一本书都买不了”

默认的事务传播属性就是,propagation = Propagation.REQUIRED

表示使用A方法传播到B中的事务,若B方法执行的过程中,只要有抛出异常,整个A方法都要回滚,这是默认

—————————————

可以通过propagation属性设置事务的传播行为,现在,不用默认属性了。改为

propagation = Propagation.REQUIRES_NEW

表示在B方法执行中不使用A方法传播到B中的事务,而是开启一个新事务,即使用B本身的事务。
若B方法执行的过程中,只要有抛出异常,B事务方法回滚,A不会回滚,即不影响A中的其他事务的执行(如第二次买书失败影响一次买书成成功)

修改代码,重新测试

继续用上面的测试代码测试,提醒一下,现在用户余额是120,他买的起第一本书,买不起第二本

bookController.checkout(new int[]{1,2},1);

测试后,数据库数据发生了改变,也就是从以前的”有一本书不能买,那就一本书都买不了”,变成了现在的“能买几本买几本”。


5、事务失效

 数据库一次执行数据的单元要么这个事务(执行的sql)都成功,要么失败

 spring事务生效的前提是,service必须是一个bean对象
  • 方法不是public的
@Transactional只能用于public的方法上,否则会失效
调用该类自己的方法,而没有经过spring的代理类,默认只有在外部调用事务才会生效
  • 异常被吃
 将异常try catch 没有throw出来。回滚异常没有,无法回滚
spring默认只会回滚非检查异常和error异常
如果是抛出受检异常,需要在注解@Transactional加属性rollbackFor
因为spring事务是用动态代理实现,因此如果方法使用了final修饰,则代理类无法对目标方法进行重写,植入事务功能

====================

spring的事务是声明式事务,而声明式事务的本质是SpringAOP,SpringAOP的本质动态代理。事务要生效必须代理对象在调用。

自调用问题

通过this调用同一个service中的方法,this是指service实现类对象本身,不是代理对象,就相当于方法中的代码粘到了大方法里面,相当于还是一个方法。(会导致事务失效)

@Override
@Transactional
public void bigSave(SpuVo spuVo) {
    /// 1.保存spu相关
    // 1.1. 保存spu基本信息 spu_info
    Long spuId=saveSpu();
    // 1.2. 保存spu的描述信息 spu_info_desc
    this.saveSpuDesc(spuVo, spuId);
    //1.3、保存spu的基本属性
    this.saveSpuBaseAttrs(spuVo, spuId);
    
    // 2. 保存sku相关信息
    this.saveSkus(spuVo, spuId);
    //给mq发送消息
    this.sendMessage(spuId);
}

自调用问题解决办法

通过其他service对象(spuDescService)调用,这个service对象本质动态代理对象

自调用问题解决办法

this.方法名()替换this代理对象.方法名()即可

类中获取代理对象分三个步骤

  1. 导入aop的场景依赖:spring-boot-starter-aop

  2. 开启AspectJ的自动代理,同时要暴露代理对象:@EnableAspectJAutoProxy(exposeProxy=true)

  3. 获取代理对象:SpuInfoService proxy = (SpuInfoService) AopContext.currentProxy();


 数据库连接池

1、DataSource

数据源使用 javax.sql.DataSource来表示,DataSource只是一个接口,该接口通常由第三方提供实现。

springboot中经常能看到这样的配置

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

实际上,在JDBC内核API的实现下,就已经可以实现对数据库的访问了,那么我们为什么还需要数据源呢?主要出于以下几个目的:

  1. 封装关于数据库访问的各种参数,实现统一管理
  2. 通过对数据库的连接池管理,节省开销并提高效率

数据库连接池(connection pool概念本质上是个集合容器里面存放着数据库的连接。

  • 系统初始化时,创建一定数量的连接对象放入连接池。
  • 当有需要时,从连接池中获取空闲的连接对象,对数据库进行操作
  • 使用完毕后,将该连接对象归还至连接池,方便后续复用

数据库连接池的设计思想:消除频繁创建对象和释放资源带来的延迟提高系统性能


2、Hikari 数据源

Springboot默认支持的4种数据源定义

org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration 分别是:

对于这4种数据源,当 classpath 下有相应的类存在时,Springboot 会通过自动配置为其生成DataSource Bean,DataSource Bean默认只会生成一个,四种数据源类型的生效先后顺序如下:Tomcat–> Hikari –> Dbcp –> Dbcp2 。

注:springboot1.0时数据源默认使用的是tomcat,2.0以后换成了hikari

Hikari的配置参数就是spring.datasource.hikari.*形式

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 5
      connection-test-query: SELECT 1 FROM DUAL
      maximum-pool-size: 20
      auto-commit: true
      idle-timeout: 30000
      pool-name: SpringBootDemoHikariCP
      max-lifetime: 60000
      connection-timeout: 30000

这些配置的含义:


3、Druid数据源

还可以选择使用其他第三方的数据源,例如:Druid、c3p0等。以使用Druid数据源为例

<!-- druid数据源驱动 -->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.1.6</version>
</dependency>

定义数据
使用注解@Bean 创建一个DataSource Bean并将其纳入到Spring容器中进行管理即可。

spring:
  datasource:
    druid:
      # 数据库访问配置, 使用druid数据源
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: oracle.jdbc.driver.OracleDriver
      url: jdbc:oracle:thin:@localhost:1521:ORCL
      username: test
      password: 123456
      # 连接池配置
      initial-size: 5
      min-idle: 5
      max-active: 20
      # 连接等待超时时间
      max-wait: 30000
      # 配置检测可以关闭的空闲连接间隔时间
      time-between-eviction-runs-millis: 60000
      # 配置连接在池中的最小生存时间
      min-evictable-idle-time-millis: 300000
      validation-query: select '1' from dual
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      pool-prepared-statements: true
      max-open-prepared-statements: 20
      max-pool-prepared-statement-per-connection-size: 20
      # 配置监控统计拦截filters, 去掉监控界面sql无法统计, 'wall'用于防火墙
      filters: stat,wall
      # Spring监控AOP切入点,如x.y.z.service.*,配置多个英文逗号分隔
      aop-patterns: com.springboot.servie.*

切换默认数据源,不需要吗???通过在核心配置中通过spring.datasource.type属性指定数据源的类型

参数校验

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.7.Final</version>
</dependency>

@NotNull :作用于基本数据类型
@NotEmpty    作用于集合
@NotBlank    作用于字符串

功能:检验Controller的入参是否符合规范需要搭配@Valid使用

 @NotNull(message = "cid不能为空")
    protected Integer cid;
 
    @NotBlank(message = "班级名称不能为空")
    protected String cname;
public void addBook(@RequestBody @Valid Book book)

对同一个Model,我们在增加和修改时对参数的校验也是不一样的,这个时候我们就需要定义分组验证

原文地址:https://blog.csdn.net/m0_56799642/article/details/133469576

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_29184.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注