JDBC基础入门(2)

时间:2022-05-04
本文章向大家介绍JDBC基础入门(2),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

其他关于C3P0的详细内容, 可参考C3P0主页.

HikariCP

HikariCP是另一款高性能/”零开销”/高品质的数据库连接池,据测试,其性能优于C3P0(详细信息可参考号称性能最好的JDBC连接池:HikariCP),但国内HikariCP资料不多,其项目主页为https://github.com/brettwooldridge/HikariCP,使用HikariCP需要在pom.xml中添加如下依赖:

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

HikariCP用方法获取Connection的方法与C3P0大同小异:

public static DataSource getDataSourceHikari(String file) {
    if (dataSource == null) {
        synchronized (ConnectionManger.class) {
            if (dataSource == null) {
                Properties properties = SQLUtil.loadConfig(file);
                HikariConfig config = new HikariConfig();
                config.setDriverClassName(properties.getProperty("mysql.driver.class"));
                config.setJdbcUrl(properties.getProperty("mysql.url"));
                config.setUsername(properties.getProperty("mysql.user"));
                config.setPassword(properties.getProperty("mysql.password"));
                // 设置连接池最大连接数
                config.setMaximumPoolSize(Integer.valueOf(properties.getProperty("pool.max.size")));
                // 设置连接池最少连接数
                config.setMinimumIdle(Integer.valueOf(properties.getProperty("pool.min.size")));
                // 设置最大空闲时间
                config.setIdleTimeout(Integer.valueOf(properties.getProperty("pool.max.idle_time")));
                // 设置连接最长寿命
                config.setMaxLifetime(Integer.valueOf(properties.getProperty("pool.max.life_time")));
                dataSource = new HikariDataSource(config);
            }
        }
    }
    return dataSource;
}
public static Connection getConnectionHikari(String file) {
    return getConnection(getDataSourceHikari(file));
}

附:

1. ConnectionManger与SQLUtil完整代码地址;

2. properties文件形式如下:

## Data Source
mysql.driver.class=com.mysql.jdbc.Driver
mysql.url=jdbc:mysql://host:port/database
mysql.user=user
mysql.password=password
pool.max.size=20
pool.min.size=3
pool.init.size=10
pool.max.statements=180
pool.max.idle_time=60
pool.max.life_time=1000

SQL执行

Statement

java.sql.Statement可用于执行DDL/DML/DCL语句:

Executes the given SQL statement, which may return multiple results.

Java 1.7还新增了closeOnCompletion()方法,当所有依赖于当前Statement的ResultSet关闭时,该Statement自动关闭.

executeUpdate

Statement使用executeUpdate方法执行DDL/DML(不包含select)语句:执行DDL该方法返回0; 执行DML返回受影响的记录数.

DDL

@Test
public void ddlClient() throws SQLException {
    try (
            Connection connection = ConnectionManger.getConnectionHikari("common.properties");
            Statement statement = connection.createStatement()
    ) {
        int res = statement.executeUpdate("CREATE TABLE t_ddl(" +
                "id INT auto_increment PRIMARY KEY, " +
                "username VARCHAR(64) NOT NULL, " +
                "password VARCHAR (36) NOT NULL " +
                ")");
        System.out.println(res);
    }
}

DML

@Test
public void dmlClient() throws SQLException {
    try (
            Connection connection = ConnectionManger.getConnectionHikari("common.properties");
            Statement statement = connection.createStatement()
    ) {
        int res = statement.executeUpdate("INSERT INTO " +
                "t_ddl(username, password) " +
                "SELECT name, password FROM user");
        System.out.println(res);
    }
}

execute

execute方法几乎可以执行任何SQL语句,但较为繁琐(除非不清楚SQL语句类型,否则不要使用execute方法).该方法返回值为boolean,代表执行该SQL语句是否返回ResultSet,然后Statement提供了如下方法来获取SQL执行的结果:

Retrieves the current result as a ResultSet object.

SQLUtil

public class SQLUtil {
    // ...
    public static void executeSQL(Statement statement, String sql) {
        try {
            // 如果含有ResultSet
            if (statement.execute(sql)) {
                ResultSet rs = statement.getResultSet();
                ResultSetMetaData meta = rs.getMetaData();
                int columnCount = meta.getColumnCount();
                for (int i = 1; i <= columnCount; ++i) {
                    System.out.printf("%st", meta.getColumnName(i));
                }
                System.out.println();
                while (rs.next()) {
                    for (int i = 1; i <= columnCount; ++i) {
                        System.out.printf("%st", rs.getObject(i));
                    }
                    System.out.println();
                }
            } else {
                System.out.printf("该SQL语句共影响%d条记录%n", statement.getUpdateCount());
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

client

@Test
public void executeClient() throws SQLException {
    try(
            Connection connection = SQLUtil.getConnection("common.properties");
            Statement statement = connection.createStatement()
            ){
        SQLUtil.executeSQL(statement, "UPDATE t_ddl SET username = 'feiqing'");
        SQLUtil.executeSQL(statement, "SELECT * FROM t_ddl");
    }
}

PreparedStatement

PreparedStatement是Statement的子接口, 它可以预编译SQL语句,编译后的SQL模板被存储在PreparedStatement对象中,每次使用时首先为SQL模板设值,然后执行该语句(因此使用PreparedStatement效率更高).

创建PreparedStatement需要使用Connection的prepareStatement(String sql)方法,该方法需要传入SQL模板,可以包含占位符参数:

PreparedStatement statement = connection.prepareStatement("INSERT INTO t_ddl(username, password) VALUES (?, ?)")

1

1

PreparedStatement也提供了excute等方法来执行SQL语句, 只是这些方法无须传入参数, 因为SQL语句已经存储在PreparedStatement对象中.

由于执行SQL前需要为SQL模板传入参数值,PreparedStatement提供了一系列的setXxx(int parameterIndex, X x)方法;另外,如果不清楚SQL模板各参数的类型,可以使用setObject(int parameterIndex, Object x)方法传入参数, 由PreparedStatement来负责类型转换.

@Test
public void comparisonPrepared() throws SQLException {
    Connection connection = null;
    try {
        connection = SQLUtil.getConnection("common.properties");
        long start = System.currentTimeMillis();
        try (Statement statement = connection.createStatement()) {
            for (int i = 0; i < 1000; ++i) {
                statement.executeUpdate("INSERT INTO t_ddl(username, password) VALUES ('name" + i + "','password" + i + "')");
            }
        }
        long mid = System.currentTimeMillis();
        try (PreparedStatement statement = connection.prepareStatement("INSERT INTO t_ddl(username, password) VALUES (?, ?)")) {
            for (int i = 0; i < 1000; ++i) {
                statement.setString(1, "name" + i);
                statement.setObject(2, "password" + i);
                statement.execute();
            }
        }
        long end = System.currentTimeMillis();
        System.out.printf("Statement: %d%n", mid - start);
        System.out.printf("Prepared:  %d%n", end - mid);
    } finally {
        try {
            assert connection != null;
            connection.close();
        } catch (SQLException e) {
        }
    }
}

注意: SQL语句的占位符参数只能代替普通值, 不能代替表名/列名等数据库对象, 更不能代替INSERT/SELECT等关键字.

使用PreparedStatement还有另外一个优点:使用PreparedStatement无须拼接SQL字符串,因此可以防止SQL注入(关于SQL注入的问题可参考SQL Injection, 现代的ORM框架都解决了该问题).

注:

1. 默认使用PreparedStatement是没有开启预编译功能的,需要在URL中给出useServerPrepStmts=true参数来开启此功能;

2. 当使用不同的PreparedStatement对象来执行相同SQL语句时,还是会出现编译两次的现象,这是因为驱动没有缓存编译后的函数key,导致二次编译.如果希望缓存编译后的函数key,那么就要设置cachePrepStmts=true参数.

3. 另外, 还可以设置预编译缓存的大小:cachePrepStmts=true&prepStmtCacheSize=50&prepStmtCacheSqlLimit=300`

jdbc:mysql://host:port/database?useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=50&prepStmtCacheSqlLimit=300

CallableStatement

在数据库中创建一个简单的存储过程add_pro:

mysql> delimiter //
mysql> CREATE PROCEDURE add_pro(a INT, b INT, OUT sum INT)
    -> BEGIN
    -> SET sum = a + b;
    -> END
    -> //
mysql> delimiter ;

delimiter //会将SQL语句的结束符改为//, 这样就可以在创建存储过程时使用;作为分隔符. MySQL默认使用;作为SQL结束符.

调用存储过程需要使用CallableStatement,可以通过Connection的prepareCall()方法来创建,创建时需要传入调用存储过程的SQL语句,形式为:

{CALL procedure_name(?, ?, ?)}

存储过程的参数既有入参,也有回参; 入参可通过setXxx(int parameterIndex/String parameterName, X x)方法传入;回参可以通过调用registerOutParameter(int parameterIndex, int sqlType)来注册, 经过上面步骤, 就可以调用execute()方法来调用该存储过程, 执行结束, 则可通过getXxx(int parameterIndex/String parameterName)方法来获取指定回参的值:

@Test
public void callableClient() throws SQLException {
    try (
            Connection connection = SQLUtil.getConnection("common.properties");
            CallableStatement statement = connection.prepareCall("{CALL add_pro(?, ?, ?)}")
    ) {
        // statement.setInt("a", 1);
        statement.setInt(1, 11);
        // statement.setInt("b", 2);
        statement.setInt(2, 22);
        // 注册CallableStatement回参
        statement.registerOutParameter(3, Types.INTEGER);
        // 执行存储过程
        statement.execute();
        // statement.getInt(3);
        System.out.printf("存储过程执行结果为: %d%n", statement.getInt("sum"));
    }
}