Spring Boot入门9

回顾:
JDBC: JAVA DataBase Conectivity java数据库连接,为了连接各种数据库,有一个统一的接口

JDBC开发步骤:
    1.注册驱动  Class.forName("类的全路径")
    2.建立连接  DriverManager.getConnection(url,username,password);
    3.通过连接获取执行SQL的对象 
            Statement connection.createStatement() : 若使用这个对象做CRUD的操作,会出现sql注入的问题
            PreparedStatement connection.prepareStatement(sql) : 可以解决sql注入问题,能够对SQL语句进行预编译
                sql = insert into user values(null,?,?);
                prepareStatement.setXXX(第几个问号,参数) XXX表示的是具体的某种类型
    4.执行SQL语句
            executeQuery  : 执行查询操作,返回的ResultSet结果集
            executeUpdate : 执行增删改的操作,返回的是影响的行数 
    5.对结果进行处理
            ResultSet
                while(rs.next()){
                    rs.getXXX("列名")
                }
    6.释放一些资源
            connection.close()
            statement.close()
            resultSet.close();


DBUtils工具集
    1.创建核心类: QueryRunner(连接池)
    2.执行增删改的方法: qr.update(sql,params...) ; //需要连接,它会从构造函数中传入的池子中获取一个连接,执行完之后,又归还到池子中
    3.执行查询操作:        qr.query(sql,ResultSetHandler,params...)
                ResultSetHandler 对应的实现类:
                        BeanHandler<User>(User.class)     : 将查询结果的第一行记录封装到一个javabean中
                        BeanListHandler<User>(User.class) : 将查询结果的每一行记录封装到一个javabean中,并且将每个javabean保存到一个List集合中

连接池:
    由于创建连接会很消耗资源,所以我们采取了一种程序已启动就创建若干个连接,放到一个池子中保存
    当用户需要使用的时候,就从池子中直接获取已经建立好的连接
    用户用完之后,不是直接释放连接,而是归还到池子中去

    c3p0:
        配置文件:c3p0-config.xml
        保存的位置: resources
        使用: ComboPooledDataSource ds= new ComboPooledDataSource();
        注意: 一个程序,通常只会创建一个池子

能够使用 c3p0+dbutils 对数据库表数据进行增删改查     


 事务: 一组SQL操作,要么都成功,要么都失败

 事务的开启: 
    start transaction
    java : conn.setAutoCommit(false)
 事务的提交/回滚
    commit/rollback
    conn.commit()/conn.rollback();

转账的案例

事务的特性:
    原子性: 事务是最小不可分割的单元, 要么都成功,要么都失败
    一致性: 事务执行前后,数据的状态要保持一致, 转账前后数据库金额一致
    隔离性: 一个事务的执行,不受其他事务的影响
    持久性: 事务一旦提交,数据就持久保存到磁盘中

若不考虑隔离级别,会出现什么问题
    脏读: 一个事务读取到另外一个事务没有提交的数据
    不可重复度: 同一个事务多次读取,数据不一致(update)
    虚读/幻读:同一个事务多次读取,数据不一致(insert)

隔离级别:
    read uncommitted : 读取未提交, 脏读,不可重复度, 虚度
    read committed : 读取已提交, 不可重复度, 虚读
    repeatable read : 可重复读   虚读
    serializable : 串行化


写的问题: 并发更新
    悲观锁:比较悲观,一上来直接加锁
    乐观锁:比较乐观
                查询的时候,会记录下一个version
                当数据进行提交的时候,会重新查询一次当前记录的版本号
                    若版本不一致,重新将这条记录查询出来,重新修改记录,再次提交
                    若版本一致,直接提交


ThreadLocal : 线程局部变量
    set(value)  : map.put(Thread.currentThread(),value);
    get()       : map.get(Thread.currentThread())
    remove()    : map.remove(Thread.currentThread())

H2数据库 : 了解

反射: 框架中使用的较多
    字段:Field
        getFields() 
        getDeclaredFields();

        getField(name)
        getDeclaredField(name)
    Field.set(obj,value)

    方法:
        getMethods
        getDeclaredMehtods

        getMethod(名字,参数类型)
        getDeclaredMethods(名字,参数类型)
    method.invoke(实例,参数)


静态代理:
    1.代理类和被代理类需要继承同一个类或者实现同一个方法
    2.代理类需要持有被代理类的引用
    3.增强需要增强的方法
    4。不需要增强的就直接调用原来的

一、事务

1. 事务介绍

事务可以包含多个操作步骤 , 如果有一个步骤失败,那么这一组都以失败告终。

事务是一组操作,这组操作要么都成功,要么都失败

事务是指包含多个微小逻辑单元的一组操作, 只要其中有一个逻辑失败了,那么这一组操作就全部以失败告终,不存在一半成功,一半不成功的状况。

事务在平常的CRUD当中也许不太常用, 但是如果我们有一种需求,要求,一组操作中,必须全部成功执行,才算完成任务,只要有一个出错了,那么所有的任务都将回到最初的状况,恢复原样。那么这就可以使用事务了。如: 银行的转账例子 , 又如一次性往两张表添加记录,需要确保这两张表都能全部成功添加,不允许一张表成功,一张表失败这种情况出现。

2. 事务入门

create database day09;

use day09;
create table user(
    id int primary key auto_increment,
    name varchar(20),
    money double
);

insert into user values(null,'jack',2000);
insert into user values(null,'rose',2000);

1. 命令行演示

  • 方式一

这里写图片描述

-- 开启事务
start transaction;

-- 执行操作 
update student set age = 28 where id = 6; 

-- 提交事务,只有提交事务,数据才会真的保存到底层设备上。
commit;

-- 如果不想提交,想回到最初的状态,那么可以回滚事务.
rollback; 
  • 方式二

mysql 的事务设置是自动提交,我们可以关闭掉自动提交开关,然后手动提交事务。

这里写图片描述

-- 显示有关提交的变量信息
show variables like '%commit%';

默认的情况下:
开启事务
update user set money=3000;
提交了事务

-- 关闭自动提交
set autocommit = off  ;  或者写成  set autocommit = 0 ; 

-- 执行操作,可以不用开启事务
update student set age = 28 where id = 6; 

-- 必须提交,才能看到结果
commit;

2. 代码演示

 @Test
    public void testTransaction(){
        Connection conn = null;
        try {
            ComboPooledDataSource dataSource = new ComboPooledDataSource();

            conn = dataSource.getConnection();

            //开启事务 关闭自动提交,
            conn.setAutoCommit(false);

            //3. 执行语句
            String sql = "insert into student values(null ,?,?)";
            PreparedStatement ps = conn.prepareStatement(sql);


            ps.setString(1,"lisi2");
            ps.setInt(2,19);

            ps.executeUpdate();

            //提交事务
            conn.commit();


            //释放资源..回收连接对象
            ps.close();
            conn.close();

        } catch (Exception e) {
            e.printStackTrace();

            //如果出现了异常,那么回滚事务
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }

    }

3. 事务特性 ACID

  • 原子性

原子性(Atomicity) : 事务中的逻辑要全部执行,不可分割。(原子是物理中最小单位)

  • 一致性

一致性(Consistency):事务执行的结果必须是使数据库数据从一个一致性状态变到另外一种一致性状态

  • 隔离性(isolation)

一个事务的执行过程中不能影响到其他事务的执行,即一个事务内部的操作及使用的数据对其他事务是隔离的,并发执行各个事务之间无不干扰。

  • 持久性()

即一个事务一旦提交,它对数据库数据的改变是永久性的。之后的其它操作不应该对其执行结果有任何影响。

4. 事务隔离级别

主要是用来解决事务并发执行,引发的问题。

如果两个事务同时,或者交错执行,那么他们的执行结果可能会受对方影响,这会导致数据的前后显示不一致。所以为了保证并发操作数据的正确性及一致性,SQL规范于1992年提出了数据库事务隔离级别

事务的并发主要有两个方面的问题 : 读的问题 | 写的问题 , 相对于写的问题,读的问题出现的几率更高些。

1. 安全隐患

如果事务没有任何隔离设置,那么在并发情况会出现以下问题。

a. 读的问题
  • 脏读

脏读: 指 一个事务 读到了另一个事务还未提交的数据 :读已提交

  • 不可重复读

一个事务读到了另一个事务提交的更新的数据, 导致多次查询结果不一致。(针对update)

  • 虚读|幻读

一个事务读到了另一个事务已提交的插入的数据,导致多次查询结果不一致。(针对 insert)

b. 写的问题

写的问题其实就只有一个,就是丢失更新

丢失更新:指一个事务去修改数据库, 另一个事务也修改数据库,最后的那个事务,不管是提交还是回滚都会造成前面一个事务的数据更新丢失

  • 解决办法:

    1. 悲观锁

    还没干活就想着会丢失更新。 查询数据的后面跟上关键字 for update

    1. 乐观锁

    程序员自己控制,在表里面增加一个字段version (版本的意思), 默认是0 。 只要有一次操作,这个版本就递增。下次来操作,先对版本号,如果对不上,表示是旧的数据,得先查询。

2. 隔离级别

a. 读未提交
  1. 打开两个dos终端, 设置A窗口的隔离级别为 读未提交

这里写图片描述

  1. 两个窗口都分别开启事务

这里写图片描述

读未提交: 一个事务可以读取到另一个事务没有提交的数据

待解决: 3个(脏读、不可重复读、虚读)

解决:0个

b. 读已提交
  1. 设置A窗口的隔离级别为 读已提交

这里写图片描述

  1. A B 两个窗口都开启事务, 在B窗口执行更新操作。

这里写图片描述

  1. 在A窗口执行的查询结果不一致。 一次是在B窗口提交事务之前,一次是在B窗口提交事务之后。

这里写图片描述

读已提交:一个事务能读取到另一个事务已经提交的数据,但是未提交的数据读取不到了。

解决: 脏读

待解决: 不可重复 , 虚读

c. 可重复读

Repeatable Read: set session transaction isolation level repeatable read;

解决: 脏读 、不可重复读

待解决: 虚读(但是其实mysql已经在底层把这个问题给避免了。)

d. 序列化 | 串行化

serializable: set session transaction isolation level serializable;

  • 按功能大小 排序 : 序列化 > 可重复读 > 读已提交 > 读未提交
  • 按效率来排序: 序列化 < 可重复读 < 读已提交 < 读未提交

MySql: 默认隔离级别: 可重复读

Oracle: 读已提交

演示(了解)  : 开启两个窗口, 在两个窗口中设置隔离级别

    update account set money =520 where name = 'jack';

    演示脏读的发生
        将数据库的隔离级别设为read uncommitted
            set session transaction isolation level read uncommitted;
        查看隔离级别
            select @@tx_isolation;

    解决脏读
        将数据库的隔离级别设为read committed
            set session transaction isolation level read committed;
        不能避免不可重复读和幻读的发生

    解决不可重复读:
        将数据库的隔离级别设为repeatable read  
            set session transaction isolation level repeatable read;

    演示串行化
        将数据库的隔离级别设为serializable 
            set session transaction isolation level serializable;

5. 事务管理

该小节讲述的是在三层结构里,如何优雅的声明事务和管理事务。

1. 三层结构介绍

三层结构其实是指把项目代码按照不同的逻辑、功能划分出来的层级,包含 控制层、业务逻辑层 、 数据访问层 ,对应着我们平常见到的 controller | service | dao 。 每个层级有各自的功能作用,

controller : 负责接收处理页面请求 和 响应

service : 负责处理业务逻辑(如: 数据封装、逻辑判断等)

dao: 负责和数据库打交道(增删改查)

  • 此处以用户注册来演示三层代码结构

    • controller
  public class UserController {
      private static final String TAG = "UserController";

      @PostMapping("/users")
      public String register(User user){
          try {
              UserService userService = new UserServiceImpl();
              userService.register(user);
          } catch (SQLException e) {
              e.printStackTrace();
          }
          return "注册成功";
      }
  }
  • service
  public interface UserService {
      void register(User user) throws SQLException;
  }

  ------------------下面是具体的实现----------------------
  public class UserServiceImpl implements UserService {
      @Override
      public void register(User user) throws SQLException {
          UserDao userDao = new UserDaoImpl();

          //可以在这里对注册的密码进行加密, 逻辑上的校验
          userDao.save(user);
      }
  }
  • dao

    “`java
    public interface UserDao {
    void save(User user) throws SQLException;
    }

    ——————–下面是具体的实现—————————-
    public class UserDaoImpl implements UserDao {
    private static final String TAG = “UserDaoImpl”;

    @Override
    public void save(User user) throws SQLException {

      QueryRunner runner = new QueryRunner(C3P0Util.getDataSource());
      String sql = "insert into user values(null , ? ,? ,?,?)";
      runner.update(sql , 
                    user.getUsername() , 
                    user.getPassword(),
                    user.getEmail(),
                    user.getPhone());
    

    }
    }


#### 2. 三层结构中的事务管理

> 上面已经给大家演示过了未来写代码的一个雏形, 那么在这种三层体系中,我们该如何管理事务呢? 要想解答这个疑问,首先得想想,有关事务的代码应该在哪一层编写?

* 事务应该位于哪一个层级? 

> 事务操作应该在service层中实现, controller是用于接收和响应请求的,至于这个请求中间是怎么运作的,它其实是不关心的。 那么为什么不能是dao层呢? 因为dao层的每一个动作,仅仅是表示和数据库的一次交互而已,如果我们一个请求需要完成两个或者两个以上的数据库操作,那么在dao层里面事务就无法处理了。反之,service层主要适用于表示业务,那么这个业务可以调用调用一次dao层的方法,也可以调用多次方法,它们都可以看成是一个整体,不可分割。那么在service层囊括起来正好。 这有一个必须要注意的点: 要求service层和dao层使用的连接对象是同一个。

![这里写图片描述](https://img-blog.csdn.net/20180914192344603?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d3dzI5NDk5Mzc0MQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

##### a. 传递Connection

> 上面已经说明了,需要在service层开启事务,那么如何确保dao层使用的连接对象和开启事务的连接对象是同一个,可以通过参数传递connection 对象

* service

```java
public class UserServiceImpl implement UserService{

    /**
     * 转账
     */
    public void transfer()  {

        Connection conn = null;
        try {
            //1. 获取连接对象
           conn = C3P0Util.getConn();

            //2. 开启事务,其实就是关闭自动提交
            conn.setAutoCommit(false);

            //3. 调用dao层方法
            UserDao userDao = new UserDaoImpl();

            userDao.outMoney(conn,"zhangsan",100);
            userDao.inMoney(conn,"lisi",100);

            //4. 提交事务
            conn.commit();
        } catch (SQLException e) {
            e.printStackTrace();

            //5. 如果有异常,那么回滚事务
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }

    }
}




<div class="se-preview-section-delimiter"></div>
  • dao

public  class UserDaoImpl implement UserDao{
    //转入
    @Override
    public void inMoney(Connection conn , String name, int money) throws SQLException {
        QueryRunner runner = new QueryRunner();
        String sql = "update user set money = money + ? where name = ?";
        runner.update(conn , sql , money , name);

    }

    //转出
    @Override
    public void outMoney(Connection conn , String name, int money) throws SQLException {
        QueryRunner runner = new QueryRunner();
        String sql = "update user set money = money - ? where name = ?";
        runner.update(conn , sql , money , name);
    }
}




<div class="se-preview-section-delimiter"></div>
b. 使用ThreadLocal

前面使用参数形式来传递Conenction,也不是不行。但是如果想让代码写得更加优雅,那么还是得使用ThreadLocal 。 ThreadLocal 直译过来的意思是: 线程本地。她可以用来存储数据,供当前的线程调用,底层其实使用Map集合来存数据。map的Key 是当前现成对象, value 值就是我们存储的数据了。ThreadLocal 不能用来做线程间通讯或者共享内容 ,它所存的值只针对当前线程有效。

上面的例子,从service开始,直到两个dao方法的执行完毕,都是在一个线程内工作,那么我们可以在service层就把connection对象存储到ThradLocal里面,在dao层取出来使用即可。

一句话概括这个ThreadLocal的作用: 就是可以往里面存东西,然后可以在整个线程里面取出来。

  • C3P0Util

    private static  ThreadLocal<Connection> threadLocal  = new ThreadLocal<Connection>();

    public static Connection getConnTL(){
        try {

            Connection conn = threadLocal.get();
            if(conn == null){
                conn = dataSource.getConnection();
                threadLocal.set(conn);
            }
            //2. 获取连接
            return conn;
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }




<div class="se-preview-section-delimiter"></div>
  • service
/**
     * 转账
     */
    public void transfer()  {

        Connection conn = null;
        try {
            //1. 获取连接对象  , 当第一次调用该方法的时候,Connction已经存储到ThreadLocal中去了。
           conn = C3P0Util.getConnTL();

            //2. 开启事务,其实就是关闭自动提交
            conn.setAutoCommit(false);

            //3. 调用dao层方法
            UserDao userDao = new UserDaoImpl();

            userDao.outMoney("zhangsan",100);
            userDao.inMoney("lisi",100);

            //4. 提交事务
            conn.commit();
        } catch (SQLException e) {
            e.printStackTrace();

            //5. 如果有异常,那么回滚事务
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }

    }




<div class="se-preview-section-delimiter"></div>
  • dao
  //转入
    @Override
    public void inMoney( String name, int money) throws SQLException {
        QueryRunner runner = new QueryRunner();
        String sql = "update user set money = money + ? where name = ?";
        runner.update(C3P0Util.getConnTL() , sql , money , name);
    }

    //转出
    @Override
    public void outMoney( String name, int money) throws SQLException {
        QueryRunner runner = new QueryRunner();
        String sql = "update user set money = money - ? where name = ?";
        runner.update(C3P0Util.getConnTL() , sql , money , name);
    }




<div class="se-preview-section-delimiter"></div>

二、H2数据库

1. H2介绍

h2是一个开源的纯java编写的轻量级数据库,是一个用Java开发的嵌入式数据库,只有一个jar文件,可以直接嵌入到应用项目中。
h2最大的用途在于可以同应用程序打包在一起发布,这样可以非常方便地存储少量结构化数据,它的另一个用途是用于单元测试。
启动速度快,而且可以关闭持久化功能,每一个用例执行完随即还原到初始状态。h2的第三个用处是作为缓存,作为NoSQL的一个补充。

  • 产品优势
    • 纯Java编写,不受平台的限制;
    • 只有一个jar文件,适合作为嵌入式数据库使用;
    • h2提供了一个十分方便的web控制台用于操作和管理数据库内容;
    • 功能完整,支持标准SQL和JDBC。麻雀虽小五脏俱全;
    • 支持内嵌模式、服务器模式和集群。

2. 下载 安装

  • 地址

http://www.h2database.com/html/download.html

  • 目录介绍
 h2
  |---bin
  |    |---h2-1.1.116.jar   //H2数据库的jar包(驱动也在里面)
  |    |---h2.bat              //Windows控制台启动脚本
  |    |---h2.sh                  //Linux控制台启动脚本
  |    |---h2w.bat              //Windows控制台启动脚本(不带黑屏窗口)
  |---docs                       //H2数据库的帮助文档(内有H2数据库的使用手册)
  |---service //通过wrapper包装成服务。
  |---src //H2数据库的源代码
  |---build.bat //windows构建脚本
  |---build.sh //linux构建脚本




<div class="se-preview-section-delimiter"></div>
  • 启动连接

3. H2运行模式

  1. 内嵌模式

内嵌模式下,应用和数据库同在一个JVM中,通过JDBC进行连接。 可持久化,但同时只能一个客户端连接。内嵌模式性能会比较好
这里写图片描述

  1. 服务器模式

使用服务器模式和内嵌模式一样,只不过它可以跑在另一个进程里。

这里写图片描述

  1. 混合模式

第一个应用以内嵌模式启动它,对于后面的应用来说它是服务器模式跑着的

这里写图片描述

3. java 操作

添加依赖:
testCompile group: 'com.h2database', name: 'h2', version: '1.4.197'





<div class="se-preview-section-delimiter"></div>
  • 嵌入式连接
 Connection conn = DriverManager.getConnection("jdbc:h2:D:/aa/test", "sa", "");




<div class="se-preview-section-delimiter"></div>
  • 远程连接
 Connection conn = DriverManager.
       getConnection("jdbc:h2:tcp://localhost/D:/aa/test", "sa", "");




<div class="se-preview-section-delimiter"></div>
  • 内存数据库
   Connection conn = DriverManager.
       getConnection("jdbc:h2:tcp://localhost/mem:test2", "sa", "");

猜你喜欢

转载自blog.csdn.net/www294993741/article/details/82707769
今日推荐