zoukankan      html  css  js  c++  java
  • JDBC

    jdbc简介

    JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。

    接口和数据库驱动

    使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。具体的JDBC驱动是由数据库厂商提供的,访问某个具体的数据库,我们只需要引入该厂商提供的JDBC驱动,就可以通过JDBC接口来访问,这样保证了Java程序编写的是一套数据库访问代码,却可以访问各种不同的数据库,因为他们都提供了标准的JDBC驱动。

    从代码来看,Java标准库自带的JDBC接口其实就是定义了一组接口,而某个具体的JDBC驱动其实就是实现了这些接口的类:

    截屏2020-08-19 10.07.38

    截屏2020-08-19 10.08.45

    实际上,一个MySQL的JDBC的驱动就是一个jar包,它本身也是纯Java编写的。我们自己编写的代码只需要引用Java标准库提供的java.sql包下面的相关接口,由此再间接地通过MySQL驱动的jar包通过网络访问MySQL服务器,所有复杂的网络通讯都被封装到JDBC驱动中,因此,Java程序本身只需要引入一个MySQL驱动的jar包就可以正常访问MySQL服务器:

    截屏2020-08-19 10.09.41

    jdbc的好处

    • 各数据库厂商使用相同的接口,Java代码不需要针对不同数据库分别开发;
    • Java程序编译期仅依赖java.sql包,不依赖具体数据库的jar包;
    • 可随时替换底层数据库,访问数据库的Java代码基本不变。

    jdbc连接

    添加依赖

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
        <scope>runtime</scope> <!--编译Java程序并不需要MySQL的这个jar包,只有在运行期才需要使用-->
    </dependency>
    

    创建数据库和表

    用一个sql脚本

    -- 创建数据库learjdbc:
    DROP DATABASE IF EXISTS learnjdbc;
    CREATE DATABASE learnjdbc;
    
    -- 创建登录用户learn/口令learnpassword
    CREATE USER IF NOT EXISTS learn@'%' IDENTIFIED BY 'learnpassword';
    GRANT ALL PRIVILEGES ON learnjdbc.* TO learn@'%' WITH GRANT OPTION;
    FLUSH PRIVILEGES;
    
    -- 创建表students:
    USE learnjdbc;
    CREATE TABLE students (
      id BIGINT AUTO_INCREMENT NOT NULL,
      name VARCHAR(50) NOT NULL,
      gender TINYINT(1) NOT NULL,
      grade INT NOT NULL,
      score INT NOT NULL,
      PRIMARY KEY(id)
    ) Engine=INNODB DEFAULT CHARSET=UTF8;
    
    -- 插入初始数据:
    INSERT INTO students (name, gender, grade, score) VALUES ('小明', 1, 1, 88);
    INSERT INTO students (name, gender, grade, score) VALUES ('小红', 1, 1, 95);
    INSERT INTO students (name, gender, grade, score) VALUES ('小军', 0, 1, 93);
    INSERT INTO students (name, gender, grade, score) VALUES ('小白', 0, 1, 100);
    INSERT INTO students (name, gender, grade, score) VALUES ('小牛', 1, 2, 96);
    INSERT INTO students (name, gender, grade, score) VALUES ('小兵', 1, 2, 99);
    INSERT INTO students (name, gender, grade, score) VALUES ('小强', 0, 2, 86);
    INSERT INTO students (name, gender, grade, score) VALUES ('小乔', 0, 2, 79);
    INSERT INTO students (name, gender, grade, score) VALUES ('小青', 1, 3, 85);
    INSERT INTO students (name, gender, grade, score) VALUES ('小王', 1, 3, 90);
    INSERT INTO students (name, gender, grade, score) VALUES ('小林', 0, 3, 91);
    INSERT INTO students (name, gender, grade, score) VALUES ('小贝', 0, 3, 77);
    

    在控制台输入mysql -u root -p,输入root口令后以root身份,把上述SQL贴到控制台执行一遍就行。如果你运行的是最新版MySQL 8.x,需要调整一下CREATE USER语句。

    连接

    Connection代表一个JDBC连接,它相当于Java程序到数据库的连接(通常是TCP连接)。打开一个Connection时,需要准备URL、用户名和口令,才能成功连接到数据库。

    url:

    //jdbc:mysql://<hostname>:<port>/<db>?key1=value1&key2=value2
    jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8
    

    获取数据库连接:

    // JDBC连接的URL, 不同数据库有不同的格式:
    String JDBC_URL = "jdbc:mysql://localhost:3306/test";
    String JDBC_USER = "root";
    String JDBC_PASSWORD = "password";
    // 获取连接:
    try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);){
      // TODO: 访问数据库...
    } // 自动关闭连接
    

    核心代码是DriverManager提供的静态方法getConnection()DriverManager会自动扫描classpath,找到所有的JDBC驱动,然后根据我们传入的URL自动挑选一个合适的驱动。

    CRUD

    Create,Retrieve,Update and Delete

    查询

    第一步,通过Connection提供的createStatement()方法创建一个Statement对象,用于执行一个查询;

    第二步,执行Statement对象提供的executeQuery("SELECT * FROM students")并传入SQL语句,执行查询并获得返回的结果集,使用ResultSet来引用这个结果集;

    第三步,反复调用ResultSetnext()方法并读取每一行结果。

    try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
        try (Statement stmt = conn.createStatement()) {
            try (ResultSet rs = stmt.executeQuery("SELECT id, grade, name, gender FROM students WHERE gender=1")) {
                while (rs.next()) {
                    long id = rs.getLong(1); // 注意:索引从1开始
                    long grade = rs.getLong(2);
                    String name = rs.getString(3);
                    int gender = rs.getInt(4);
                }
            }
        }
    }
    

    StatmentResultSet都是需要关闭的资源,因此嵌套使用try (resource)确保及时关闭;

    rs.next()用于判断是否有下一行记录,如果有,将自动把当前行移动到下一行(一开始获得ResultSet时当前行不是第一行);

    ResultSet获取列时,索引从1开始而不是0

    必须根据SELECT的列的对应位置来调用getLong(1)getString(2)这些方法,否则对应位置的数据类型不对,将报错。

    SQL注入

    使用Statement拼字符串非常容易引发SQL注入的问题,这是因为SQL参数往往是从方法参数传入的。

    SELECT * FROM user WHERE login='bob' OR pass=' AND pass=' OR pass=''
    

    作为参数传入后变成查询name = "bob' OR pass=", pass = " OR pass='"

    //todo 没理解sql注入

    避免SQL注入攻击,一个办法是针对Statement中的所有字符串参数进行转义,还有一个办法就是使用PreparedStatement,它可以完全避免SQL注入的问题,PreparedStatementStatement更安全,而且更快。

    因为PreparedStatement始终使用?作为占位符,并且把数据连同SQL本身传给数据库,这样可以保证每次传给数据库的SQL语句是相同的,只是占位符的数据不同,还能高效利用数据库本身对查询的缓存。

    使用PreparedStatement必须首先调用setObject()设置每个占位符?的值,最后获取的仍然是ResultSet对象。

    try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
        try (PreparedStatement ps = conn.prepareStatement("SELECT id, grade, name, gender FROM students WHERE gender=? AND grade=?")) {
            ps.setObject(1, "M"); // 注意:索引从1开始
            ps.setObject(2, 3);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    long id = rs.getLong("id");
                    long grade = rs.getLong("grade");
                    String name = rs.getString("name");
                    String gender = rs.getString("gender");
                }
            }
        }
    }
    

    数据类型转换

    JDBC在java.sql.Types定义了一组常量来表示如何映射SQL数据类型:

    SQL数据类型 Java数据类型
    BIT, BOOL boolean
    INTEGER int
    BIGINT long
    REAL float
    FLOAT, DOUBLE double
    CHAR, VARCHAR String
    DECIMAL BigDecimal
    DATE java.sql.Date, LocalDate
    TIME java.sql.Time, LocalTime

    小结

    JDBC接口的Connection代表一个JDBC连接;

    使用JDBC查询时,总是使用PreparedStatement进行查询而不是Statement

    查询结果总是ResultSet,即使使用聚合查询也不例外。

    插入

    也是用PreparedStatement执行一条SQL语句,不过最后执行的不是executeQuery(),而是executeUpdate()

    try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
        try (PreparedStatement ps = conn.prepareStatement(
                "INSERT INTO students (id, grade, name, gender) VALUES (?,?,?,?)")) {
            ps.setObject(1, 999); // 注意:索引从1开始
            ps.setObject(2, 1); // grade
            ps.setObject(3, "Bob"); // name
            ps.setObject(4, "M"); // gender
            int n = ps.executeUpdate(); // 1
        }
    }
    

    如果数据库的表设置了自增主键,那么在执行INSERT语句时,并不需要指定主键,数据库会自动分配主键。如何在插入后获取自增后的主键呢?

    try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
        try (PreparedStatement ps = conn.prepareStatement(
                "INSERT INTO students (grade, name, gender) VALUES (?,?,?)",
                Statement.RETURN_GENERATED_KEYS)) {//注意必须加入参数
            ps.setObject(1, 1); // grade
            ps.setObject(2, "Bob"); // name
            ps.setObject(3, "M"); // gender
            int n = ps.executeUpdate(); // 1
          	//获取主键
            try (ResultSet rs = ps.getGeneratedKeys()) {
                if (rs.next()) {
                    long id = rs.getLong(1); // 注意:索引从1开始
                }
            }
        }
    }
    

    如果一次插入多条记录,那么ResultSet对象就会有多行返回值。

    更新

    try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
        try (PreparedStatement ps = conn.prepareStatement("UPDATE students SET name=? WHERE id=?")) {
            ps.setObject(1, "Bob"); // 注意:索引从1开始
            ps.setObject(2, 999);
            int n = ps.executeUpdate(); // 返回更新的行数
        }
    }
    

    删除

    try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
        try (PreparedStatement ps = conn.prepareStatement("DELETE FROM students WHERE id=?")) {
            ps.setObject(1, 999); // 注意:索引从1开始
            int n = ps.executeUpdate(); // 删除的行数
        }
    }
    

    jdbc事务

    数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列,有点类似于Java的synchronized同步。数据库系统保证在一个事务中的所有SQL要么全部执行成功,要么全部不执行,即数据库事务具有ACID特性:

    • Atomicity:原子性
    • Consistency:一致性
    • Isolation:隔离性
    • Durability:持久性

    数据库事务可以并发执行,而数据库系统从效率考虑,对事务定义了不同的隔离级别。

    Isolation Level 脏读(Dirty Read) 不可重复读(Non Repeatable Read) 幻读(Phantom Read)
    Read Uncommitted Yes Yes Yes
    Read Committed - Yes Yes
    Repeatable Read - - Yes
    Serializable - - -

    MySQL的默认隔离级别是REPEATABLE READ

    在JDBC中执行事务,本质上就是如何把多条SQL包裹在一个数据库事务中执行。

    Connection conn = openConnection();
    try {
        // 关闭自动提交:
        conn.setAutoCommit(false);
        // 执行多条SQL语句:
        insert(); update(); delete();
        // 提交事务:
        conn.commit();
    } catch (SQLException e) {
        // 回滚事务:
        conn.rollback();
    } finally {
        conn.setAutoCommit(true);
        conn.close();
    }
    

    开启事务的关键代码是conn.setAutoCommit(false),表示关闭自动提交。默认情况下,我们获取到Connection连接后,总是处于“自动提交”模式,也就是每执行一条SQL都是作为事务自动执行。

    提交事务的代码在执行完指定的若干条SQL语句后,调用conn.commit()

    要注意事务不是总能成功,如果事务提交失败,会抛出SQL异常(也可能在执行SQL语句的时候就抛出了),此时我们必须捕获并调用conn.rollback()回滚事务。

    最后,在finally中通过conn.setAutoCommit(true)Connection对象的状态恢复到初始值。

    设定事务的隔离级别

    // 设定隔离级别为READ COMMITTED:
    conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
    

    jdbc Batch

    只有参数不同的若干语句可以作为batch执行,即批量执行,这种操作有特别优化,速度远远快于循环执行每个SQL。如批量插入

    try (PreparedStatement ps = conn.prepareStatement("INSERT INTO students (name, gender, grade, score) VALUES (?, ?, ?, ?)")) {
        // 对同一个PreparedStatement反复设置参数并调用addBatch():
        for (Student s : students) {
            ps.setString(1, s.name);
            ps.setBoolean(2, s.gender);
            ps.setInt(3, s.grade);
            ps.setInt(4, s.score);
            ps.addBatch(); // 添加到batch
        }
        // 执行batch:
        int[] ns = ps.executeBatch();
        for (int n : ns) {
            System.out.println(n + " inserted."); // batch中每个SQL执行的结果数量
        }
    }
    

    连接池

    执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。

    JDBC连接池有一个标准的接口javax.sql.DataSource,常用的JDBC连接池有:

    • HikariCP(最广泛)
    • C3P0
    • BoneCP
    • Druid

    HikariCP使用:

    1.添加依赖

    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>2.7.1</version>
    </dependency>
    

    2.创建一个DataSource实例,这个实例就是连接池

    HikariConfig config = new HikariConfig();
    config.setJdbcUrl("jdbc:mysql://localhost:3306/test");
    config.setUsername("root");
    config.setPassword("password");
    config.addDataSourceProperty("connectionTimeout", "1000"); // 连接超时:1秒
    config.addDataSourceProperty("idleTimeout", "60000"); // 空闲超时:60秒
    config.addDataSourceProperty("maximumPoolSize", "10"); // 最大连接数:10
    DataSource ds = new HikariDataSource(config);
    

    3.获取Connection

    有了连接池以后,获取Connection时,只需要把jdbc中的DriverManage.getConnection()改为ds.getConnection()

    try (Connection conn = ds.getConnection()) { // 在此获取连接
        ...
    } // 在此“关闭”连接
    

    通过连接池获取连接时,并不需要指定JDBC的相关URL、用户名、口令等信息,因为这些信息已经存储在连接池内部了(创建HikariDataSource时传入的HikariConfig持有这些信息)。

    第一次调用ds.getConnection(),会迫使连接池内部先创建一个Connection,再返回给客户端使用。当我们调用conn.close()方法时(在try(resource){...}结束处),不是真正“关闭”连接,而是释放到连接池中,以便下次获取连接时能直接返回。

    因此,连接池内部维护了若干个Connection实例,如果调用ds.getConnection(),就选择一个空闲连接,并标记它为“正在使用”然后返回,如果对Connection调用close(),那么就把连接再次标记为“空闲”从而等待下次调用。这样一来,我们就通过连接池维护了少量连接,但可以频繁地执行大量的SQL语句。

    参考:
    https://www.liaoxuefeng.com/wiki/1252599548343744/1255943820274272

  • 相关阅读:
    在windows下如何批量转换pvr,ccz为png或jpg
    cocos2d-x 中的 CC_SYNTHESIZE 自动生成 get 和 set 方法
    制作《Stick Hero》游戏相关代码
    触摸事件的setSwallowTouches()方法
    随机生成数(C++,rand()函数)
    随机生成数
    cocos2d-x 设置屏幕方向 横屏 || 竖屏
    Joystick 摇杆控件
    兔斯基 经典语录
    Cocos2d-x 3.2 EventDispatcher事件分发机制
  • 原文地址:https://www.cnblogs.com/chzhyang/p/13528208.html
Copyright © 2011-2022 走看看