乐趣区

关于java:SpringBoot-2x系列Spring-JDBC

JDBC 介绍

从这篇文章开始,咱们将会介绍 SpringBoot 另外一个外围的技术,即数据库拜访技术,提到数据拜访,学习 Java 的同学霎时能就想起 JDBC 技术,JDBC 是 Java Database Connectivity 的全称, 是 Java 语言中用来标准客户端程序如何来拜访数据库的利用程序接口,提供了诸如查问和更新数据库中数据的一套规范的 API,这套规范不同的数据库厂家之间独特准守,并提供各自的具体实现。如图所示:

这样设计的益处,就是 Java 程序只须要和 JDBC API 交互,从而屏蔽了拜访数据库的简单的实现,大大降低了 Java 程序拜访数据库的复杂度。对于日常开发而言,咱们只须要把握 JDBC API 标准中的几个外围编程对象即可,这些对象包含 DriverManger、DataSource、Connection、Statement,及 ResultSet。

DriverManager

DriverManager 次要负责加载不同数据库厂家提供的驱动程序包(Driver),并且依据不同的申请向 Java 程序返回数据库连贯(Connection)对象,先看下 Driver 接口的定义:

public interface Driver {
 // 获取数据库连贯
 Connection connect(String url, java.util.Properties info)
 throws SQLException;
 boolean acceptsURL(String url) throws SQLException;
 DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
 throws SQLException;
 int getMajorVersion();
 int getMinorVersion();
 boolean jdbcCompliant();
 public Logger getParentLogger() throws SQLFeatureNotSupportedException;}

Driver 中有个重要的办法 connect,来提供 Connection 对象

不同的数据库对 Driver,有具体的实现,以 MySql 为例:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
 // 通过 DriverManager 注册 Driver
 static {
 try {java.sql.DriverManager.registerDriver(new Driver());
 } catch (SQLException E) {throw new RuntimeException("Can't register driver!");
 }
 }
 …
}

这里用到了 DriverManager,DriverManager 通过 registerDriver 来注册不同数据库的 Driver,并且还提供了 getConnection 返回数据库连贯对象。

Connection

通过 DriverManager 能够获取 Connetion 对象,Connection 对象能够了解与数据库连贯的一种会话(Session),一个 Connection 对象代表一个数据库的连贯,负责实现与数据库底层的通信。

Connection 对象提供了一组重载的办法来创立 Statement 和 PreparedStatement,Statement 和 PreparedStatement 是 SQL 执行的载体,另外 Connection 对象还会波及事务相干的操作。

Connection 对象最外围的几个办法如下:

public interface Connection  extends Wrapper, AutoCloseable {
 // 创立 Statement
 Statement createStatement() throws SQLException;
 // 创立 PreparedStatement
 PreparedStatement prepareStatement(String sql) throws SQLException;
 // 提交
 void commit() throws SQLException;
 // 回滚
 void rollback() throws SQLException;
 // 敞开连贯
 void close() throws SQLException;}

Statement/PreparedStatement

Statement 和 PreparedStatement 是由 Connection 对象来创立的,用来执行动态的 SQL 语句并且返回生成的后果集对象,这里存在两种类型,一种是一般的 Statement,另外一种反对预编译的 PreparedStatement。

所谓预编译,是指数据库的编译器会对 SQL 语句提前编译,而后将预编译的后果缓存到数据库中,下次执行时就能够通过替换参数并间接应用编译过的语句,从而大大提高 SQL 的执行效率。

以 Statement 为例,看下 Statement 最外围的办法:

public interface Statement extends Wrapper, AutoCloseable {
 // 执行查问语句
 ResultSet executeQuery(String sql) throws SQLException; 
 // 执行更新语句
 int executeUpdate(String sql) throws SQLException; 
 // 执行 SQL 语句
 boolean execute(String sql) throws SQLException; 
 // 执行批处理
 int[] executeBatch() throws SQLException;
}

ResultSet

通过 Statement 或 PreparedStatement 执行 SQL 语句,咱们引出了另外一个对象即为 ResultSet 对象,代码如下:

public interface ResultSet extends Wrapper, AutoCloseable {
 // 获取下一个后果
 boolean next() throws SQLException;
 // 获取某一个类型的后果值
 Value getXXX(int columnIndex) throws SQLException;
 …
}

ResultSet 对象提供了 next() 办法,用来对整个后果集遍历操作,如果 next() 办法返回为 true,阐明还有下一条记录,

咱们能够调用 ResultSet 对象的一系列 getXXX() 办法来获得对应的后果值。

JDBC 拜访数据库流程

对于开发人员而言,通过 JDBC 的 API 是 Java 拜访数据库的次要路径,上面用代码来展现下拜访数据库的一个整体流程:

String url = "jdbc:mysql://localhost:3306/test" ;
String username = "root" ;
String password = "root" ;
​
//1. 通过 DriverManager 获取 connection 连贯
Connection connection = DriverManager.getConnection(url,username,password);
​
//2. 创立 preparedStatement
PreparedStatement preparedStatement = connection.prepareStatement("select * from user");
​
//3. 执行 SQL 返回 ResultSet
ResultSet resultSet = preparedStatement.executeQuery();
​
//4. 遍历 resultSet 后果集
while (resultSet.next()){//resultSet.getString("1");
}
​
//5. 开释资源
resultSet.close();
preparedStatement.close();
connection.close();

配置数据源

下面咱们在介绍 JDBC 的时候,Connection 对象是通过 DriverManager 获取,Connection 对象代表着和数据库的连贯,每次通过 DriverManager 获取比拟耗时,影响了零碎的性能。那有没有方法可能复用 Connection 对象呢,答案是必定的

JDBC 给咱们提供了 DataSource 接口来实现 Connection 的复用,外围代码如下:

public interface DataSource  extends CommonDataSource, Wrapper {Connection getConnection() throws SQLException;
 
 Connection getConnection(String username, String password)
 throws SQLException;
}

作为一种根底组件,不须要开发人员本人实现 DataSource,因为业界曾经存在了很多优良的实现计划,如 DBCP、C3P0、Druid、Hikari 等

SpringBoot 默认 HikariDataSource 作为 DataSource 的实现,当初咱们 SpringBoot 为例,看下 SpringBoot 如何通过 JDBC 来操作数据库的,在进行数据库操作之前,咱们首先须要先配置 DataSource,SpringBoot 配置 DataSource 非常简单,只须要在配置文件中增加 DataSource 的配置:

spring:
 # datasource 数据源配置内容
 datasource:
 url: jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8
 driver-class-name: com.mysql.cj.jdbc.Driver
 username: root
 password: root

应用 JDBC 操纵数据库

DataSource 配好后,咱们在本地的数据库服务中,创立一个 test 数据库,并且执行以下 DDL 创立 user 表

CREATE TABLE `user` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
 `username` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名',
 `password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '明码',
 `create_time` datetime DEFAULT NULL COMMENT '创立工夫',
 PRIMARY KEY (`id`),
 UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

接下来咱们创立一个实体类,实体类中属性和 user 表中字段一一对应

@Data
public class User {
    /**
     * 主键
     */
    private Integer id;
    /**
     * 用户名
     */
    private String username;
    /**
     * 明码
     */
    private String password;
    /**
     * 创立工夫
     */
    private Date createTime;

}

留神:这里应用了 Lombok 的 @Data 注解来生成 get/set 办法。

咱们再定义一个 UserDao 接口,

public interface UserDao {
 /**
 *  新增
 * @param user
 * @return
 */
 Integer insert(User user);
 /**
 *  依据 ID 查问
 * @param id
 * @return
 */
 User selectById(Integer id);
 /**
 *  依据 ID 更新
 * @param user
 * @return
 */
 Integer updateById(User user);
 /**
 *  依据 ID 删除
 * @param id
 * @return
 */
 Integer deleteById(Integer id);
}

这里之所以要抽离出一个 UserDao 一层有两个起因:第一 UserDao 只封装了对 use 表的数据库操作,代码易于保护和治理,第二咱们能够基于 UserDao 接口提供不同的实现来拜访数据库,比方咱们能够提供基于原生 JDBC 的实现,也能够用 JDBCTemplate 实现数据库的拜访,还能够通过 Mybatis 等

接下来将通过代码模式来展现下 SpringBoot 是如何通过 JDBC API 对数据库进行 CRUD 操作的。咱们来定义 UserDao 的具体实现类命名为:UserRawJdbcDao 实现以下办法:

新增数据

@Override
 public Integer insert(User user) {final String SQL_INSERT = "INSERT INTO user(username, password, create_time) VALUES(?, ?, ?)";
 Connection connection = null;
 PreparedStatement statement = null;
 ResultSet rs = null;
 Integer count = 0;
 try{connection = dataSource.getConnection();
 statement = connection.prepareStatement(SQL_INSERT, Statement.RETURN_GENERATED_KEYS);
 statement.setString(1,user.getUsername());
 statement.setString(2,user.getPassword());
 statement.setTimestamp(3,new Timestamp(user.getCreateTime().getTime()));
 count = statement.executeUpdate();
 rs = statement.getGeneratedKeys();
 if(rs.next()){user.setId(rs.getInt(1));
 }
 }catch (SQLException e){e.printStackTrace();
 }finally {
 try {if(rs != null){rs.close();
 }
 if(statement != null){statement.close();
 }
 if(connection != null){connection.close();
 }
​
 } catch (SQLException e) {e.printStackTrace();
 }
​
 }
 return count;
 }

查问数据

@Override
    public User selectById(Integer id) {
        final String SQL_SELECT_ID = "SELECT id,username,password,create_time FROM user WHERE id = ?";
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet rs = null;
        User user = null;
        try{connection = dataSource.getConnection();
            statement = connection.prepareStatement(SQL_SELECT_ID);
            statement.setInt(1, id);
            rs = statement.executeQuery();
            if(rs.next()){user = new User();
                user.setId(rs.getInt("id"));
                user.setUsername(rs.getString("username"));
                user.setPassword(rs.getString("password"));
                user.setCreateTime(rs.getTimestamp("create_time"));
            }
        }catch (SQLException e){e.printStackTrace();
        }finally {
            try {if(rs != null){rs.close();
                }
                if(statement != null){statement.close();
                }
                if(connection != null){connection.close();
                }

            } catch (SQLException e) {e.printStackTrace();
            }
        }
        return user;
    }

更新数据

@Override
 public Integer updateById(User user) {
 final String SQL_UPDATE = "UPDATE user SET username = ?, password = ? WHERE id = ?";
 Connection connection = null;
 PreparedStatement statement = null;
 ResultSet rs = null;
 Integer count = 0;
​
 try{connection = dataSource.getConnection();
 statement = connection.prepareStatement(SQL_UPDATE);
 statement.setString(1,user.getUsername());
 statement.setString(2,user.getPassword());
 statement.setInt(3,user.getId());
 count = statement.executeUpdate();}catch (SQLException e){e.printStackTrace();
 }finally {
 try {if(rs != null){rs.close();
 }
 if(statement != null){statement.close();
 }
 if(connection != null){connection.close();
 }
 } catch (SQLException e) {e.printStackTrace();
 }
 }
 return count;
 }

删除数据

@Override
 public Integer deleteById(Integer id) {
 final String SQL_DELETE = "DELETE FROM user WHERE id = ?";
 Connection connection = null;
 PreparedStatement statement = null;
 ResultSet rs = null;
 Integer count = 0;
 try{connection = dataSource.getConnection();
 statement = connection.prepareStatement(SQL_DELETE);
 statement.setInt(1,id);
 count = statement.executeUpdate();}catch (SQLException e){e.printStackTrace();
 }finally {
 try {if(rs != null){rs.close();
 }
 if(statement != null){statement.close();
 }
 if(connection != null){connection.close();
 }
​
 } catch (SQLException e) {e.printStackTrace();
 }
 }
 return count;
 }

到此,SpringBoot 通过调用原生的 JDBC 的 API 实现对 user 表的 CRUD 操作,这代码对有代码洁癖的同学几乎不能忍,有大量共性的代码,如创立 Connection、Statement、ResultSet、资源的开释和异样的解决。这部分封装和优化 SpringBoot 曾经解决过了,SpringBoot 提供了 JdbcTemplate 模板工具类实现数据拜访,它简化了 JDBC API 的应用办法。

应用 JdbcTemplate 操纵数据库

同 UserRawJdbcDao,咱们再定义 UserDao 的另外一套实现类命名为:UserJdbcDao,这套实现类是通过 JdbcTemplate 实现对数据库的操作,实现接口定义的办法如下:

新增数据

@Override
public Integer insert(User user){
 
 // 创立 KeyHolder 对象,设置返回的主键 ID
 KeyHolder keyHolder = new GeneratedKeyHolder();
 int count = jdbcTemplate.update(INSERT_PREPARED_STATEMENT_CREATOR_FACTORY.newPreparedStatementCreator(Arrays.asList(user.getUsername(),user.getPassword(),user.getCreateTime())),keyHolder);
 // 设置 ID 主键到 entity 实体中
 if (keyHolder.getKey() != null) {user.setId(keyHolder.getKey().intValue());
 }
 // 返回影响行数
 return count;
}

查问数据

 @Override
 public User selectById(Integer id){
 User result = jdbcTemplate.queryForObject("SELECT id, username, password, create_time FROM user WHERE id=?",
 new BeanPropertyRowMapper<>(User.class), id);
 return result;
 }

更新数据

 @Override
 public Integer updateById(User user) {
 return jdbcTemplate.update("UPDATE user SET username = ?, password = ? WHERE id = ?",
 user.getUsername(),user.getPassword(),user.getId());
 }

删除数据

@Override
 public Integer deleteById(Integer id){return jdbcTemplate.update("DELETE FROM user WHERE id = ?", id);
 }

小结

通过比照咱们发现应用 JdbcTemplate 模板工具类能够大大减少 JDBC 拜访数据库的代码复杂度,作为开发人员咱们应该只关怀业务逻辑的具体实现过程,对 JDBC 底层对象的创立,资源的开释,异样的捕捉,应该交给框架对立保护和治理。

尽管 JdbcTemplate 缩小的咱们拜访数据库的代码量,不过应用也有一些问题,比方:新增数据的时候默认无奈返回生成主键的 id,将 SQL 硬编码到 Java 代码中,如果 SQL 批改,须要从新编译 Java 代码,不利于零碎的保护等。这时咱们须要另外一个框架,它就是赫赫有名的 Mybatis,下一篇我将会介绍 SpringBoot 如何整合 Mybatis。

我的项目源码

github:https://github.com/dragon8844/springboot-learning/tree/main/jdbc-template

最初说一句

如果这篇文章对您有所帮忙,或者有所启发的话,帮忙关注一下,您的反对是我保持写作最大的能源,多谢反对。

此外,关注公众号:彩色的灯塔,专一 Java 后端技术分享,涵盖 Spring,Spring Boot,SpringCloud,Docker,Kubernetes 中间件等技术。

退出移动版