JavaWEB-03-JDBC
内容
1. JDBC入门
1.1 概述
- 
概念 JDBC:Java DataBase Connectivity,Java数据库连接JDBC就是使用Java语言操作关系型数据库的一套API。Sun公司和数据库厂商共同制定的一套连接并操作数据库的统一规范(接口),由数据库厂商负责实现(驱动Jar包);我们使用的时候只需要导入数据库厂商已经实现好的jar包即可。
- 
好处 因为各个数据库厂商都实现了同一套 JDBC接口规范。- 我们可以使用同一套Java代码操作不同的关系型数据库
- 访问数据库的Java代码不改变的前提下,替换不同的驱动Jar包可以轻松更换数据库
 
- 我们可以使用同一套
- 
我们要怎么做 整体思路:基于 JDBC定义的规则编码,不用考虑不同数据库之间的差异- 导数对应据库驱动包。
- 基于JDBC定义的规则轻松编程。
 
1.2 quick start
- 
实现步骤 - 导入Jar包
- 注册驱动
- 获取连接
- 获取执行者对象
- 执行SQL语句,并且接收结果
- 处理结果
- 释放资源
 
- 导入
- 
演示代码 package com.cy.jdbc; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.Statement; /** * JDBC快速入门 */ public class JDBCDemo { public static void main(String[] args) throws Exception { //1. 注册驱动 建议写上去 Class.forName("com.mysql.jdbc.Driver"); //2. 获取连接 //url jdbc:mysql:///maven == jdbc:mysql://127.0.0.1:3306/maven //jdbc:mysql://ip:数据库端口号/数据库名称?添加自定义参数 String url = "jdbc:mysql://127.0.0.1:3306/maven"; String username = "root"; String password = "root"; //Connection 数据库的连接对象 Connection conn = DriverManager.getConnection(url, username, password); //3. 定义sql String sql = "update account set money = ? where id = ?"; //4. 获取执行sql的对象 Statement //Statement stmt = conn.createStatement(); PreparedStatement pps = conn.prepareStatement(sql); pps.setDouble(1,3000); pps.setInt(2,1); //5. 执行sql //int count = stmt.executeUpdate(sql);//受影响的行数 执行增删改 int count = pps.executeUpdate(); //6. 处理结果 受影响的行数 System.out.println(count); //7. 释放资源 pps.close(); conn.close(); } }
2. jdbc常用API
2.1 DriverManager
功能1:注册驱动
- 不需要手动调用DriverManager的API注册,而是保证com.mysql.jdbc.Driver被加载进内存即可。
查看com.mysql.jdbc.Driver类的源码
package com.mysql.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
    static {
        try {
            // 在静态代码块中完成了注册驱动的代码
            // 我们只要保证Driver被加载进内存,下面的注册驱动的步骤就会自动完成。
            /*
            	Class.forName("com.mysql.jdbc.Driver");
            	Driver.class //Driver的字节码对象
            	new Driver(); // 多一个driver对象
            */
            /*
            	在MySQL5以上的版本,注册驱动不需要我们自己做了。
            	在mysql-connector-java-5.1.47.jar中META-INF下面services下面的
            	java.sql.Driver文件中,填写了com.mysql.jdbc.Driver全类名
            	该类的便会自动被加载进内存
            */
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}
功能2:获取连接对象
static Connection getConnection(url, username, password)
//3.获取连接
// 相当于之前使用mysql工具连接数据库
// 完整格式:jdbc:mysql://ip:数据库端口号/数据库名称?添加自定义参数
// jdbc:mysql://就是协议 相当于 http://
// url连本机  jdbc:mysql://localhost:3306/db2 简化写法jdbc:mysql:///db2 
// 可选自定义参数:useSSL=false 不是用SSL加密并取消提示
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/db1","root","root");
2.2 Connection
就像打电话,一个Connection对象,就是一通电话。
所以连接又称会话,Session。
功能1:获取执行者对象
该对象会执行SQL语句
- 
获取普通执行者对象: Statement createStatement();
- 
获取预编译执行者对象: PreparedStatement prepareStatement(String sql);
功能2:控制事务
Connection中定义了三个方法,用来控制事务。对应MySQL中的三种操作。
- 
开启: setAutoCommit(boolean autoCommit);参数为false,则开启事务。
- 
提交: commit();
- 
回滚: rollback();
功能3:释放资源
- close();
演示代码
package com.cy.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
/**
 * JDBC API 详解:Connection
 */
public class JDBCDemo3_Connection {
    public static void main(String[] args) throws Exception {
        //1. 注册驱动  建议写上去
        Class.forName("com.mysql.jdbc.Driver");
        //2. 获取连接,配置连接四要素
        //jdbc:mysql://ip:数据库端口号/数据库名称?添加自定义参数
        String url = "jdbc:mysql://127.0.0.1:3306/web22_day03_jdbc";
        String username = "root";
        String password = "root";
        Connection conn = DriverManager.getConnection(url, username, password);
        //3. 定义sql
        String sql1 = "update account set money = money - 500 where id = 1";
        String sql2 = "update account set money = money + 500 where id = 2";
        //4. 获取执行sql的对象 Statement
        Statement stmt = conn.createStatement();
        try {
            // 开启事务
            conn.setAutoCommit(false);
            //5. 执行sql
            int count1 = stmt.executeUpdate(sql1);//受影响的行数
            //6. 处理结果
            System.out.println(count1);
            int i = 3/0;
            //5. 执行sql
            int count2 = stmt.executeUpdate(sql2);//受影响的行数
            //6. 处理结果
            System.out.println(count2);
            // 提交事务
            conn.commit();
        } catch (Exception throwables) {
            // 回滚事务,手动开启事务之后,该操作可以省略。
            // 没有没有提交,他也不会自动提交,就相当于回滚了。
            // conn.rollback();
            throwables.printStackTrace();
        }
        //7. 释放资源
        stmt.close();
        conn.close();
    }
}
2.3 Statement
执行者对象,执行SQL语句并返回SQL语句执行的结果/结果集。
- 
执行 DML/DDL语句:修改(增删改)int executeUpdate(String sql);返回值int:返回影响的行数。 参数sql: insert、update、delete语句。一般不会在 Java代码中执行DDL语句,Java中主要完成对数据库中表记录的操作。
- 
执行 DQL语句:查询ResultSet executeQuery(String sql);返回值 ResultSet:封装查询的结果集。参数 sql:select语句。
- 
释放资源 立即将执行者对象释放: void close();
- 
演示代码:演示增删改 /** * statement执行dql,并获取结果集 * JDBC API 详解:Statement */ public class JDBCDemo4_Statement { /** * 执行DML语句 * @throws Exception */ @Test public void testDML() throws Exception { //1. 注册驱动 //Class.forName("com.mysql.jdbc.Driver"); //2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写 String url = "jdbc:mysql:///maven?useSSL=false"; String username = "root"; String password = "root"; Connection conn = DriverManager.getConnection(url, username, password); //3. 定义sql String sql = "update account set money = 4000 where id = 1"; //4. 获取执行sql的对象 Statement Statement stmt = conn.createStatement(); //5. 执行sql int count = stmt.executeUpdate(sql);//执行完DML语句,受影响的行数 //6. 处理结果 if(count > 0){ System.out.println("修改成功~"); }else{ System.out.println("修改失败~"); } //7. 释放资源 stmt.close(); conn.close(); } /** * 执行DDL语句 * @throws Exception */ @Test public void testDDL() throws Exception { //1. 注册驱动 //Class.forName("com.mysql.jdbc.Driver"); //2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写 String url = "jdbc:mysql:///db1?useSSL=false"; String username = "root"; String password = "1234"; Connection conn = DriverManager.getConnection(url, username, password); //3. 定义sql String sql = "drop database db2"; //4. 获取执行sql的对象 Statement Statement stmt = conn.createStatement(); //5. 执行sql int count = stmt.executeUpdate(sql);//执行完DDL语句,可能是0 //6. 处理结果 //System.out.println(count); /* if(count > 0){ System.out.println("修改成功~"); }else{ System.out.println("修改失败~"); }*/ System.out.println(count); //7. 释放资源 stmt.close(); conn.close(); } }
- 
演示代码:查询及结果集处理 public class JDBCDemo5_ResultSet { /** * 执行DQL * * @throws Exception */ @Test public void testResultSet() throws Exception { //1. 注册驱动 //Class.forName("com.mysql.jdbc.Driver"); //2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写 String url = "jdbc:mysql:///maven?useSSL=false"; String username = "root"; String password = "root"; Connection conn = DriverManager.getConnection(url, username, password); //3. 定义sql String sql = "select * from account"; //4. 获取statement对象 Statement stmt = conn.createStatement(); //5. 执行sql executeQuery ResultSet rs = stmt.executeQuery(sql); // 6.1 光标向下移动一行,并且判断当前行是否有数据 while (rs.next()) { //移动到下一行 //6.2 获取数据 getXxx() int id = rs.getInt("id"); String name = rs.getString("name"); double money = rs.getDouble("money"); System.out.println("id = " + id); System.out.println("name = " + name); System.out.println("money = " + money); } //7. 释放资源 rs.close(); stmt.close(); conn.close(); } }
2.4 查询并封装POJO对象案例
2.4.1 需求
查询account账户表数据,封装为Account对象中,并且存储到
ArrayList集合中
2.4.2 核心步骤
- 使用上述操作完成查询,查询出结果集。
- 定义实体类Account
- 查询数据,封装到Account对象中
- 将Account对象存入ArrayList集合中
2.4.3 演示代码
package com.cy.pojo;
public class Account {
    private int id;
    private String name;
    private double money;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getMoney() {
        return money;
    }
    public void setMoney(double money) {
        this.money = money;
    }
    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", money=" + money +
                '}';
    }
}
/**
 * 实现需求:查询account账户表数据,封装为Account对象中,并且存储到ArrayList集合中
 */
public class JDBCDemo5_ResultSet {
  
    /**
     * 查询account账户表数据,封装为Account对象中,并且存储到ArrayList集合中
     * 1. 定义实体类Account
     * 2. 查询数据,封装到Account对象中
     * 3. 将Account对象存入ArrayList集合中
     *
     *
     * @throws Exception
     */
    @Test
    public void testResultSet2() throws  Exception {
        //1. 注册驱动
        //Class.forName("com.mysql.jdbc.Driver");
        //2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写
        String url = "jdbc:mysql:///maven?useSSL=false";
        String username = "root";
        String password = "root";
        Connection conn = DriverManager.getConnection(url, username, password);
        //3. 定义sql
        String sql = "select * from account";
        //4. 获取statement对象   不能用(Statement)
        //Statement stmt = conn.createStatement();
        PreparedStatement pps = conn.prepareStatement(sql);
        //5. 执行sql
        //ResultSet rs = stmt.executeQuery(sql);
        ResultSet rs = pps.executeQuery();
        // 创建集合
        List<Account> list = new ArrayList<>();
        // 6.1 光标向下移动一行,并且判断当前行是否有数据
        while (rs.next()){
            Account account = new Account();
            int id = rs.getInt("id");
            String name = rs.getString("name");
            double money = rs.getDouble("money1");
            account.setId(id);
            account.setName(name);
            account.setMoney(money);
            // 存入集合
            list.add(account);
        }
        System.out.println(list);
        //7. 释放资源
        rs.close();
        pps.close();
        conn.close();
    }
}
2.5 PreparedStatement
2.5.1概述
预编译的执行者对象,执行SQL语句并返回SQL语句执行的结果集。
相对于Statement来说,PreparedStatement有额外两个作用:
- 通过转义字符避免SQL注入问题。
- 开启预编译后,通过预编译可以提高SQL语句的执行效率。
2.5.2 使用及API
- 
获取该对象 // 这里的SQL语句,不需要指定参数,而是通过?作为占位符,后面再通过setString方法把参数设置进来 PreparedStatement connection.prepareStatement(sql)
- 
设置参数 // index 从1 开始 pst.setString(int index, String paramter)
- 
执行 DML语句:修改(增删改)int executeUpdate();返回值int:返回影响的行数。 执行 DQL语句:查询ResultSet executeQuery();返回值 ResultSet:封装查询的结果集。
- 
释放资源 立即将执行者对象释放: void close();
- 
演示代码 /** * * @throws Exception */ @Test public void testPreparedStatement() throws Exception { //2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写 String url = "jdbc:mysql:///db1?useSSL=false"; String username = "root"; String password = "1234"; Connection conn = DriverManager.getConnection(url, username, password); // 接收用户输入 用户名和密码 String name = "zhangsan"; String pwd = "' or '1' = '1"; // 定义sql 预编译sql ? 占位符 String sql = "select * from tb_user where username = ? and password = ?"; // 获取pstmt对象 PreparedStatement pstmt = conn.prepareStatement(sql); // 设置?的值 pstmt.setString(1,name); pstmt.setString(2,pwd); // 执行sql ResultSet rs = pstmt.executeQuery(); // 判断登录是否成功 if(rs.next()){ System.out.println("登录成功~"); }else{ System.out.println("登录失败~"); } //7. 释放资源 rs.close(); pstmt.close(); conn.close(); }
2.5.3 原理
可以通过日志看到其运行原理及效果。
- 
准备工作1:开启预编译 想要看小预编译执行者对象的执行效果,需要先在 url中开启预编译,添加如下自定义参数即可useServerPrepStmts=true添加该参数后,完整的 URL链接如下:jdbc:mysql:///db1?useSSL=false&useServerPrepStmts=true
- 
准备工作2:打开日志,查看预编译等相关效果 在 my.ini文件末尾,添加如下内容,并重启MySQL服务。log-output=FILE general-log=1 general_log_file="D:\mysql.log" slow-query-log=1 slow_query_log_file="D:\mysql_slow.log" long_query_time=2
- 
重启 MySQL,方式1:可以打开服务窗口,手动重启 MySQL服务。方式2:在以管理员身份打开的命令行中,键入以下命令 net stop mysql # 关闭mysql服务 net start mysql # 开启mysql服务
- 
结论 通过修改查询相关代码,并查看日志,可以得出如下结论: - 在URL中开启预编译后,相同模板的SQL语句在创建预编译执行者对象的时候,就已经发送到SQL,并完成了语法检查和编译。
- 预编译执行者对象,在往SQL模板中设置参数的时候,会对参数中的特殊符号添加转义字符\,避免SQL注入
- 使用相同模板的多条不同参数的SQL语句,只会在第一次创建预编译执行者对象的时候编译一次,从而提高执行效率。
 
- 在
- 
演示代码 /** * PreparedStatement原理 * @throws Exception */ @Test public void testPreparedStatement2() throws Exception { //2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写 // useServerPrepStmts=true 参数开启预编译功能 String url = "jdbc:mysql:///db1?useSSL=false&useServerPrepStmts=true"; String username = "root"; String password = "root"; Connection conn = DriverManager.getConnection(url, username, password); // 接收用户输入 用户名和密码 String name = "zhangsan"; String pwd = "' or '1' = '1"; // 定义sql String sql = "select * from tb_user where username = ? and password = ?"; // 获取pstmt对象 PreparedStatement pstmt = conn.prepareStatement(sql); Thread.sleep(10000); // 设置?的值 pstmt.setString(1,name); pstmt.setString(2,pwd); ResultSet rs = null; // 执行sql rs = pstmt.executeQuery(); // 设置?的值 pstmt.setString(1,"aaa"); pstmt.setString(2,"bbb"); // 执行sql rs = pstmt.executeQuery(); // 判断登录是否成功 if(rs.next()){ System.out.println("登录成功~"); }else{ System.out.println("登录失败~"); } //7. 释放资源 rs.close(); pstmt.close(); conn.close(); }
3. SQL注入及解决方案
3.1. 产生的原因
使用任意用户名并配合下面的密码,登录
' or '1' = '1
普通执行者对象拼接后的SQL 语句,变成了:
select * from tb_user where username = 'xxxx' and password = '' or '1' = '1'
普通的statement只是把用户输入的用户名和密码进行了简单的拼接,拼接到了SQL语句中。
这样,用户名或密码中特殊的单子或符号就被识别成了SQL语句中的关键字或者是特殊符号。
思路:仅把' or 1=1 --  当做普通的字符串来处理,使用转义符号\,然后面的符号表示其本来的意思(现原形)。
PreparedStatement就是使用这个思路解决的。
PreparedStatement中的方法public void setString(int parameterIndex, String x) throws SQLException 
// 提前准备好长度变量,不用每次调用length()方法,效率高
int stringLength = x.length();
for(int i = 0; i < stringLength; ++i) {
    // 获取参数的每一个字符
    char c = x.charAt(i);
    
    // 判断是否是特殊符号,如果是就在前面拼一个 \  转义,让这个特殊符号表示自己原来的意思。
    switch(c) {
        case '\u0000':
            buf.append('\\');
            buf.append('0');
            break;
        case '\n':
            buf.append('\\');
            buf.append('n');
            break;
        case '\r':
            buf.append('\\');
            buf.append('r');
            break;
        case '\u001a':
            buf.append('\\');
            buf.append('Z');
            break;
        case '"':
            if (this.usingAnsiMode) {
                buf.append('\\');
            }
            buf.append('"');
            break;
        case '\'':
            buf.append('\\');
            buf.append('\'');
            break;
        case '\\':
            buf.append('\\');
            buf.append('\\');
            break;
        case '¥':
        case '₩':
            if (this.charsetEncoder != null) {
                CharBuffer cbuf = CharBuffer.allocate(1);
                ByteBuffer bbuf = ByteBuffer.allocate(1);
                cbuf.put(c);
                cbuf.position(0);
                this.charsetEncoder.encode(cbuf, bbuf, true);
                if (bbuf.get(0) == 92) {
                    buf.append('\\');
                }
            }
            buf.append(c);
            break;
        default:
            buf.append(c);
    }
}
4. 连接池
4.1 概念
初始化并维护多个连接对象,当其他地方需要数据库连接时,从连接池获取;用完之后,归还到连接池。
以此实现连接的复用,提高效率。
数据源:DataSource,内部维护了一个连接池,还提供了操作连接池的一些功能。
一般情况下,会把数据源和连接池混为一谈,把数据源喊做连接池。
常见的数据源有:C3P0、Druid。我们学习Druid。
4.2 池化思想(重要)
以空间换时间的做法。
游戏背包、新闻客户端。
- 提供更好的使用体验
- 对资源的消耗会更少。不会频繁的创建和销毁对象。
 

4.3 Druid使用
4.3.1 导包
// 导包并添加到添加了模块类库
druid-1.1.12.jar
4.3.2 配置文件
properties配置文件(名字任意),但是配置文件中key的值固定。常见参数如下:
# 连接四要素
# 数据库驱动全类名
driverClassName=com.mysql.jdbc.Driver
# URL
url=jdbc:mysql:///maven?useSSL=false&useServerPrepStmts=true
#用户名
username=root
#密码
password=root
# 其他参数
# 初始化连接数
initialSize=5
# 最大连接数
maxActive=10
# 超时时间
maxWait=3000
完整参数介绍:
| 属性 | 说明 | 建议值 | 
|---|---|---|
| url | 数据库的jdbc连接地址。一般为连接oracle/mysql。示例如下: | |
| mysql : jdbc:mysql://ip:port/dbname?option1&option2&… | ||
| oracle : jdbc:oracle:thin:@ip:port:oracle_sid | ||
| username | 登录数据库的用户名 | |
| password | 登录数据库的用户密码 | |
| initialSize | 启动程序时,在连接池中初始化多少个连接 | 10-50已足够 | 
| maxActive | 连接池中最多支持多少个活动会话 | |
| maxWait | 程序向连接池中请求连接时,超过maxWait的值后,认为本次请求失败,即连接池 | 100 | 
| 没有可用连接,单位毫秒,设置-1时表示无限等待 | ||
| minEvictableIdleTimeMillis | 池中某个连接的空闲时长达到 N 毫秒后, 连接池在下次检查空闲连接时,将 | 见说明部分 | 
| 回收该连接,要小于防火墙超时设置 | ||
| net.netfilter.nf_conntrack_tcp_timeout_established的设置 | ||
| timeBetweenEvictionRunsMillis | 检查空闲连接的频率,单位毫秒, 非正整数时表示不进行检查 | |
| keepAlive | 程序没有close连接且空闲时长超过 minEvictableIdleTimeMillis,则会执 | true | 
| 行validationQuery指定的SQL,以保证该程序连接不会池kill掉,其范围不超 | ||
| 过minIdle指定的连接个数。 | ||
| minIdle | 回收空闲连接时,将保证至少有minIdle个连接. | 与initialSize相同 | 
| removeAbandoned | 要求程序从池中get到连接后, N 秒后必须close,否则druid 会强制回收该 | false,当发现程序有未 | 
| 连接,不管该连接中是活动还是空闲, 以防止进程不会进行close而霸占连接。 | 正常close连接时设置为true | |
| removeAbandonedTimeout | 设置druid 强制回收连接的时限,当程序从池中get到连接开始算起,超过此 | 应大于业务运行最长时间 | 
| 值后,druid将强制回收该连接,单位秒。 | ||
| logAbandoned | 当druid强制回收连接后,是否将stack trace 记录到日志中 | true | 
| testWhileIdle | 当程序请求连接,池在分配连接时,是否先检查该连接是否有效。(高效) | true | 
| validationQuery | 检查池中的连接是否仍可用的 SQL 语句,drui会连接到数据库执行该SQL, 如果 | |
| 正常返回,则表示连接可用,否则表示连接不可用 | ||
| testOnBorrow | 程序 申请 连接时,进行连接有效性检查(低效,影响性能) | false | 
| testOnReturn | 程序 返还 连接时,进行连接有效性检查(低效,影响性能) | false | 
| poolPreparedStatements | 缓存通过以下两个方法发起的SQL: | true | 
| public PreparedStatement prepareStatement(String sql) | ||
| public PreparedStatement prepareStatement(String sql, | ||
| int resultSetType, int resultSetConcurrency) | ||
| maxPoolPrepareStatement PerConnectionSize | 每个连接最多缓存多少个SQL | 20 | 
| filters | 这里配置的是插件,常用的插件有: | stat,wall,slf4j | 
| 监控统计: filter:stat | ||
| 日志监控: filter:log4j 或者 slf4j | ||
| 防御SQL注入: filter:wall | ||
| connectProperties | 连接属性。比如设置一些连接池统计方面的配置。 | |
| druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 | ||
| 比如设置一些数据库连接属性: | ||
4.3.3 代码
public class DruidDemo {
    public static void main(String[] args) throws Exception {
        //1.导入jar包
        //2.定义配置文件
        //3. 加载配置文件  map集合  key-value
        Properties prop = new Properties();
        prop.load(new FileInputStream("web_day03_jdbc-demo/src/druid.properties"));
        //4. 获取连接池对象
        DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);
        //5. 获取数据库连接 Connection
        Connection connection = dataSource.getConnection();
        System.out.println(connection);
        // 这里可以调用connection的close方法
        // Druid充了connection的close方法,实现逻辑变成了回收连接,而非关闭连接
        connection.close();
        // 获取当前代码的运行路径,可以运行下面一行代码
        //System.out.println(System.getProperty("user.dir"));
    }
}

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号