Java JDBC编程

JDBC基础

JDBC的全称是Java Database Connectivity,即Java数据库链接,它是一种能够执行SQL语句的Java API。程序可经过JDBC API链接到关系数据库,并使用结构化查询语言(SQL,数据库标准的查询语言)来完成对数据库的查询、更新java

与其余数据库编程环境相比,JDBC为数据库开发提供了标准的API,使用JDBC开发的数据库应用能够跨平台运行,并且还能够跨数据库(若是所有使用标准的SQL语句)。也就是说若是使用JDBC开发一个数据库应用,则该应用既能够在Windows操做系统上运行,又能够在Unix等其余操做系统上运行,既可使用MySQL数据库,又可使用Oracle等其余的数据库,应用程序不须要作任何的修改mysql

JDBC简介

Java语言的各类跨平台特性,都采用类似的结构。由于他们都须要让相同的程序在不一样的平台上运行,因此须要中间的转换程序(为了实现Java程序的跨平台,Java为不一样的操做系统提供了不一样的Java虚拟机)。一样,为了JDBC程序能够跨平台,也须要不一样的数据库厂商提供相应的驱动程序正则表达式

clipboard.png

Sun提供的JDBC能够完成如下三个基本操做:sql

  • 创建与数据库的连接数据库

  • 执行SQL语句编程

  • 得到SQL语句的执行结果设计模式

JDBC驱动程序

数据库驱动程序是JDBC程序和数据库之间的转换层,数据库驱动程序负责将JDBC调用映射成特定的数据库调用缓存

clipboard.png

ODB,Open Database Connectivity,即开放数据库连接。ODBC和JDBC很像,严格来讲,应该是JDBC模仿了ODBC是设计。ODBC也容许应用程序经过一种通用的API访问不一样的数据库管理系统,从而使得基于ODBC的应用程序能够在不一样的数据库之间切换。一样,ODBC也须要各数据库厂商提供相应的驱动程序,而ODBC负责管理这些驱动程序安全

JDBC驱动一般有以下4种类型服务器

  • JDBC + ODBC桥的方式

  • 直接将JDBC API隐射成数据库特定的客户端API。这种驱动包含特定数据库的本地代码,用于访问特定数据库的客户端

  • 支持三层结构的JDBC访问方式,主要用于Applet阶段,经过Applet访问数据库

  • 纯java的,直接与数据库实例交互,。这种驱动是智能型的,它知道数据库使用的底层协议,是目前最流行的JDBC驱动

一般建议选择第4种JDBC驱动,这种驱动避开了本地代码,减小了应用开发的复杂性,也减小了产生冲突和出错的可能。若是对性能有严格的要求,则能够考虑使用第2种JDBC驱动,但使用这种驱动,则势必增长编码和维护的困难

JDBC比ODBC多了以下几个优点

  • ODBC更复杂,ODBC中有几个命令须要配置不少复杂的选项,而JDBC则采用简单、直观的方式来管理数据库链接

  • JDBC比ODBC安全性更高,更易部署

JDBC的经典用法

JDBC 4.2经常使用接口和类简介

JAVA8关于JDBC4.2的新增功能:
DriverManager:用于管理JDBC驱动的服务类。程序中使用该类的主要功能是获取Connection对象,该类包含以下方法

  • public static synchronized Connection getConnection(String url, String user, String password) throws SQLException:该方法得到url对应数据库的链接

Connection

Connection:表明数据库链接对象,每一个Connection表明一个物理链接会话。要想访问数据库,必须先获得数据库链接。该接口的经常使用方法以下:

  • Statement createStatement() throws SQLException:该方法返回一个Statement对象

  • PreparedStatement prepareStatement(String sql) throws SQLException:该方法返回预编译的Statement对象,即将SQL语句提交到数据库进行预编译

  • CallableStatement prepareCall(String sql) throws SQLException:该方法返回CallableStatement对象,该对象用于调用存储过程

上面三个方法都返回用于执行SQL语句的Statement对象,PreparedStatement、CallableStatement是Statement的子类,只有得到了Statement以后才能够执行SQL语句

除此以外,Connection还有以下几个用于控制事务的方法:

  • Savepoint setSavepoint() throws SQLException:建立一个保存点

  • Savepoint setSavepoint(String name):以指定名字来建立一个保存点

  • void setTransactionIsolation(int level):设置事务的隔离级别

  • void rollback():回滚事务

  • void rollback(Savepoint savepoint):将事务回滚到指定的保存点

  • void setAutoCommit(boolean autoCommit):关闭自动提交,打开事务

  • void commit() throws SQLException:提交事务

Java7位Connection新增了setSchema(String schema)、getSchema()两个方法,这两个方法用于控制该Connection访问的数据库Schema。还为Connection新增了setNetworkTimeout(Executor executor, int milliseconds)、getNetworkTimeout()两个方法来控制数据库链接的超时行为

Statement

Statement:用于执行SQL语句的工具接口。该对象既能够执行DDL、DCL语句,也能够用于执行DML语句,还能够用于执行SQL查询。当执行SQL查询时,返回查询到的结果集。它的经常使用方法以下:

  • ResultSet executeQuery(String sql) throws SQLException:该方法用于执行查询语句,并返回查询结果对应ResultSet对象。该方法只能用于执行查询语句

  • int executeUpdate(String sql) throws SQLException:该方法用于执行DML语句,并返回受影响的行数;该方法也可用于执行DDL语句,执行DDL语句将返回0

  • boolean execute(String sql) throws SQLException:该方法能够执行任何SQL语句。若是执行后第一个结果为ResultSet对象,则返回true;若是执行后第一个结果为受影响的行数或没有任何结果,则返回false

Java7为Statement新增了closeOnCompletion()方法,若是Statement执行了此方法,则当全部依赖于该Statement的ResultSet关闭时,该Statement会自动关闭。Java7还为Statement提供了一个isCloseOnCompletion()方法,该方法用于判断该Statement是否打开了“closeOnCompletion”

PreparedStatement

PreparedStatement:预编译的Statement对象,PreparedStatement是Statement的子接口,它容许数据库预编译SQL语句(这些SQL语句一般带有参数),之后每次只改变sql命令的参数,避免数据库每次都须要编译SQL语句,无需再传入SQL语句,所以性能更好。使用PreparedStatement执行SQL语句时,无须再传入SQL语句,只要为预编译的SQL语句传入参数值便可

PreparedStatement一样有executeQuery()、executeUpdate()和execute()方法,只是这三个方法无须接收SQL字符串,由于PreparedStatement对象已预编译了SQL命令,只要为这些方法传入参数便可。因此它比Statement多了以下方法:

  • void setXxx(int parameterIndex, Xxx value):该方法根据传入参数值的类型不一样,须要使用不一样的方法。传入的值根据索引传给SQL语句中指定位置的参数

ResultSet

ResultSet:结果集对象。该对象包含访问查询结果的方法,ResultSet能够经过列索引或列名得到列数据。它包含了以下经常使用方法来移动记录指针

  • void close():释放ResultSet对象

  • boolean absolute(int row):将结果集的记录指针移动到第row行,若是row是负数,则移动到倒数第row行,若是移动后的记录指针指向一条有效记录,则该方法返回true

  • void beforeFisrt():将ResultSet的记录指针定位到首行以前,这是ResultSet结果集记录指针的初始状态——记录指针的起始位置位于第一行以前。

  • boolean first():将ResultSet的记录指针定位到首行。若是移动后的记录指针指向一条有效记录,则该方法返回true

  • boolean previous():将ResultSet的记录指针定位到上一行,若是移动后的记录指针指向一条有效记录,则该方法返回true

  • boolean next():将结果集的记录指针定位到下一行,若是移动后的记录指针指向一条有效的记录,则该方法返回true

  • boolean last():将结果集的记录指针定位到最后一行,若是移动后的记录指针指向一条有效的记录,则该方法返回true

  • void afterLast():将ResultSet的记录指针定位到最后一行以后

当把记录指针移动到指定行以后,ResultSet可经过getXxx(int columnIndex)或getXxx(String columnLabel)方法来获取当前行、指定列的值,前者根据列索引获取值,后者根据列名获取值

JDBC编程步骤

1 加载数据库驱动

一般使用Class类的forName()静态方法来加载驱动

// 加载驱动,driverClass就是数据库驱动类所对应的字符串
Class.forName(driverClass);
// 加载MySQL的驱动
Class.forName("com.mysql.jdbc.Driver");
// 加载Oracle的驱动
Class.forName("oracle.jabc.driver.OracleDriver");

2 经过DriverManager获取数据库的连接

// 获取数据库链接
DriverManager.getConnection(String url, Stirng user, String pass)

当使用DriverManager来获取连接,一般须要传入三个参数:数据库URL、登陆数据库的用户名和密码

数据库URL一般遵循以下写法:jdbc是固定的;subprotocol指定链接到特定数据库的驱动;other和stuff也是不固定的

jdbc:subprotocol:other stuff

3 经过Connection对象建立Statement(或者PreparedStatement)对象

  • createStatement():建立基本的Statement对象

  • prepareStatement(String sql):根据传入的SQL语句建立预编译的Statement对象

  • prepareCall(String sql):根据传入的SQL语句建立CallableStatement对象

4 使用Statement执行SQL语句

  • execute():能够执行任何SQL语句,但比较麻烦

  • executeUpdate():主要用于执行DML和DDL语句。执行DML返回受影响的SQL语句行数,执行DDL返回0

  • executeQuery():只能执行查询语句,执行后返回表明查询结果的ResultSet对象

5 操做结果集

若是执行的SQL语句是查询语句,则执行结果将返回一个ResultSet对象,该对象里保存了SQL语句查询的结果。程序能够经过操做该ResultSet对象来取出查询结果。ResultSet对象主要提供了以下两类方法

  • next()、previous()、first()、last()、beforeFrist()、afterLast()、absolute()等移动指针的方法

  • getXxx()方法获取记录指针指向行,特定列的值。既可以使用列名做为参数可读性更好、使用索引做为参数性能更好

6 回收数据库资源

包括关闭ResultSet、Statement和Connection等资源

import java.sql.*;
public class ConnMySql
{
    public static void main(String[] args) throws Exception
    {
        // 1.加载驱动,使用反射的知识,如今记住这么写。
        Class.forName("com.mysql.jdbc.Driver");
        try(
            // 2.使用DriverManager获取数据库链接,
            // 其中返回的Connection就表明了Java程序和数据库的链接
            // 不一样数据库的URL写法须要查驱动文档知道,用户名、密码由DBA分配
            Connection conn = DriverManager.getConnection(
                "jdbc:mysql://127.0.0.1:3306/select_test"
                , "root" , "32147");
            // 3.使用Connection来建立一个Statment对象
            Statement stmt = conn.createStatement();
            // 4.执行SQL语句
            /*
            Statement有三种执行sql语句的方法:
            1 execute 可执行任何SQL语句。- 返回一个boolean值,
              若是执行后第一个结果是ResultSet,则返回true,不然返回false
            2 executeQuery 执行Select语句 - 返回查询到的结果集
            3 executeUpdate 用于执行DML语句。- 返回一个整数,
              表明被SQL语句影响的记录条数
            */
            ResultSet rs = stmt.executeQuery("select s.* , teacher_name"
                + " from student_table s , teacher_table t"
                + " where t.teacher_id = s.java_teacher"))
        {
            // ResultSet有系列的getXxx(列索引 | 列名),用于获取记录指针
            // 指向行、特定列的值,不断地使用next()将记录指针下移一行,
            // 若是移动以后记录指针依然指向有效行,则next()方法返回true。
            while(rs.next())
            {
                System.out.println(rs.getInt(1) + "\t"
                    + rs.getString(2) + "\t"
                    + rs.getString(3) + "\t"
                    + rs.getString(4));
            }
        }
    }
}

执行SQL语句的方式

使用Java8新增的executeLargeUpdate方法执行DDL和DML语句

如下程序示范了使用executeUpdate()方法(MySQL驱动暂不支持executeLargeUpdate()方法)建立数据表。该示例并无直接把数据库链接信息写在程序里,而是使用一个mysql.ini文件(properties文件)来保存数据库链接信息,这是比较成熟的作法——当须要把应用程序从开发环境移植到生产环境时,无须修改源代码,只需修改mysql.ini配置文件便可

import java.util.*;
import java.io.*;
import java.sql.*;

public class ExecuteDDL
{
    private String driver;
    private String url;
    private String user;
    private String pass;
    public void initParam(String paramFile)
        throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
    }
    public void createTable(String sql)throws Exception
    {
        // 加载驱动
        Class.forName(driver);
        try(
        // 获取数据库链接
        Connection conn = DriverManager.getConnection(url , user , pass);
        // 使用Connection来建立一个Statment对象
        Statement stmt = conn.createStatement())
        {
            // 执行DDL,建立数据表
            stmt.executeUpdate(sql);
        }
    }
    public static void main(String[] args) throws Exception
    {
        ExecuteDDL ed = new ExecuteDDL();
        ed.initParam("mysql.ini");
        ed.createTable("create table jdbc_test "
            + "( jdbc_id int auto_increment primary key, "
            + "jdbc_name varchar(255), "
            + "jdbc_desc text);");
        System.out.println("-----建表成功-----");
    }
}

下面程序执行一条insert语句,这条insert语句会向刚刚创建的jdbc_test数据表中插入几条记录。由于使用了带子查询的insert语句,因此能够一次插入多条语句

import java.util.*;
import java.io.*;
import java.sql.*;

public class ExecuteDML
{
    private String driver;
    private String url;
    private String user;
    private String pass;

    public void initParam(String paramFile)
        throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
    }
    public int insertData(String sql)throws Exception
    {
        // 加载驱动
        Class.forName(driver);
        try(
            // 获取数据库链接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection来建立一个Statment对象
            Statement stmt = conn.createStatement())
        {
            // 执行DML,返回受影响的记录条数
            return stmt.executeUpdate(sql);
        }
    }
    public static void main(String[] args)throws Exception
    {
        ExecuteDML ed = new ExecuteDML();
        ed.initParam("mysql.ini");
        int result = ed.insertData("insert into jdbc_test(jdbc_name,jdbc_desc)"
            + "select s.student_name , t.teacher_name "
            + "from student_table s , teacher_table t "
            + "where s.java_teacher = t.teacher_id;");
        System.out.println("--系统中共有" + result + "条记录受影响--");
    }
}

使用execute方法执行SQL语句

Statement的execute()方法几乎能够执行任何SQL语句,但它执行SQL语句时比较麻烦,一般没有必要使用execute()方法来执行SQL语句,使用executeQuery()或executeUpdate()方法更简单。但若是不清楚SQL语句的类型,则只能使用execute()方法来执行该SQL语句

  • getResult():获取该Statement执行查询语句所返回的ResultSet对象

  • getUpdateCount():获取该Statement()执行DML语句所影响的记录行数

import java.util.*;
import java.io.*;
import java.sql.*;

public class ExecuteSQL
{
    private String driver;
    private String url;
    private String user;
    private String pass;
    public void initParam(String paramFile)throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
    }
    public void executeSql(String sql)throws Exception
    {
        // 加载驱动
        Class.forName(driver);
        try(
            // 获取数据库链接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection来建立一个Statement对象
            Statement stmt = conn.createStatement()
            )
        {
            // 执行SQL,返回boolean值表示是否包含ResultSet
            boolean hasResultSet = stmt.execute(sql);
            // 若是执行后有ResultSet结果集
            if (hasResultSet)
            {
                try(
                    // 获取结果集
                    ResultSet rs = stmt.getResultSet()
                    )
                {
                    // ResultSetMetaData是用于分析结果集的元数据接口
                    ResultSetMetaData rsmd = rs.getMetaData();
                    int columnCount = rsmd.getColumnCount();
                    // 迭代输出ResultSet对象
                    while (rs.next())
                    {
                        // 依次输出每列的值
                        for (int i = 0 ; i < columnCount ; i++ )
                        {
                            System.out.print(rs.getString(i + 1) + "\t");
                        }
                        System.out.print("\n");
                    }
                }
            }
            else
            {
                System.out.println("该SQL语句影响的记录有"
                    + stmt.getUpdateCount() + "条");
            }
        }
    }
    public static void main(String[] args) throws Exception
    {
        ExecuteSQL es = new ExecuteSQL();
        es.initParam("mysql.ini");
        System.out.println("------执行删除表的DDL语句-----");
        es.executeSql("drop table if exists my_test");
        System.out.println("------执行建表的DDL语句-----");
        es.executeSql("create table my_test"
            + "(test_id int auto_increment primary key, "
            + "test_name varchar(255))");
        System.out.println("------执行插入数据的DML语句-----");
        es.executeSql("insert into my_test(test_name) "
            + "select student_name from student_table");
        System.out.println("------执行查询数据的查询语句-----");
        es.executeSql("select * from my_test");
    }
}

使用PreparedStatement执行SQL语句

建立PreparedStatement对象使用Connection的preparedStatement()方法,该方法须要传入一个SQL字符串,该字符串能够包含符参数

// 建立一个PreparedStatement对象
pstmt = conn.preparedStatement("insert into student_table values(null,?,1)");

PreparedStatement也提供了execute()、executeUpdate()、executeQuery()三个方法来执行SQL语句,不过这三个方法无须参数,由于PreparedStatement提供了一系列的setXxx(int index, Xxx value)方法来传入参数值

若是程序很清楚PreparedStatement预编译SQL语句中各参数的类型,则使用相应的setXxx()方法来传入参数便可;若是程序不清楚编译SQL语句中各参数的类型,则可使用setObject()方法来传入参数,由PreparedStatement来负责类型转换

下面程序示范使用Statement和PreparedStatement分别插入100条记录的对比。使用Statement须要传入100条SQL语句,但使用PreparedStatement则只需传入1条预编译的SQL语句,而后100次为该PreparedStatement的参数设值便可

import java.util.*;
import java.io.*;
import java.sql.*;

public class PreparedStatementTest
{
    private String driver;
    private String url;
    private String user;
    private String pass;
    public void initParam(String paramFile)throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
        // 加载驱动
        Class.forName(driver);
    }
    public void insertUseStatement()throws Exception
    {
        long start = System.currentTimeMillis();
        try(
            // 获取数据库链接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection来建立一个Statment对象
            Statement stmt = conn.createStatement())
        {
            // 须要使用100条SQL语句来插入100条记录
            for (int i = 0; i < 100 ; i++ )
            {
                stmt.executeUpdate("insert into student_table values("
                    + " null ,'姓名" + i + "' , 1)");
            }
            System.out.println("使用Statement费时:"
                + (System.currentTimeMillis() - start));
        }
    }
    public void insertUsePrepare()throws Exception
    {
        long start = System.currentTimeMillis();
        try(
            // 获取数据库链接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection来建立一个PreparedStatement对象
            PreparedStatement pstmt = conn.prepareStatement(
                "insert into student_table values(null,?,1)"))

        {
            // 100次为PreparedStatement的参数设值,就能够插入100条记录
            for (int i = 0; i < 100 ; i++ )
            {
                pstmt.setString(1 , "姓名" + i);
                pstmt.executeUpdate();
            }
            System.out.println("使用PreparedStatement费时:"
                + (System.currentTimeMillis() - start));
        }
    }
    public static void main(String[] args) throws Exception
    {
        PreparedStatementTest pt = new PreparedStatementTest();
        pt.initParam("mysql.ini");
        pt.insertUseStatement();
        pt.insertUsePrepare();
    }
}

SQL注入是一个较常见的Cracker入侵方式,它利用SQL语句的漏洞来入侵。如下程序以一个简单的登陆窗口为例来介绍这种SQL注入的结果。下面登陆窗口包含两个文本框,一个用于输入用户名,一个用于输入密码,系统根据用户输入与jdbc_test表里的记录进行匹配,若是找到相应记录则提示登录成功

public class LoginFrame
{
    private final String PROP_FILE = "mysql.ini";
    private String driver;
    // url是数据库的服务地址
    private String url;
    private String user;
    private String pass;
    // 登陆界面的GUI组件
    private JFrame jf = new JFrame("登陆");
    private JTextField userField = new JTextField(20);
    private JTextField passField = new JTextField(20);
    private JButton loginButton = new JButton("登陆");
    public void init()throws Exception
    {
        Properties connProp = new Properties();
        connProp.load(new FileInputStream(PROP_FILE));
        driver = connProp.getProperty("driver");
        url = connProp.getProperty("url");
        user = connProp.getProperty("user");
        pass = connProp.getProperty("pass");
        // 加载驱动
        Class.forName(driver);
        // 为登陆按钮添加事件监听器
        loginButton.addActionListener(e -> {
            // 登陆成功则显示“登陆成功”
            if (validate(userField.getText(), passField.getText()))
            {
                JOptionPane.showMessageDialog(jf, "登陆成功");
            }
            // 不然显示“登陆失败”
            else
            {
                JOptionPane.showMessageDialog(jf, "登陆失败");
            }
        });
        jf.add(userField , BorderLayout.NORTH);
        jf.add(passField);
        jf.add(loginButton , BorderLayout.SOUTH);
        jf.pack();
        jf.setVisible(true);
    }
    private boolean validate(String userName, String userPass)
    {
        // 执行查询的SQL语句
        String sql = "select * from jdbc_test "
            + "where jdbc_name='" + userName
            + "' and jdbc_desc='" + userPass + "'";
        System.out.println(sql);
        try(
            Connection conn = DriverManager.getConnection(url , user ,pass);
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(sql))
        {
            // 若是查询的ResultSet里有超过一条的记录,则登陆成功
            if (rs.next())
            {
                return true;
            }
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        return false;
    }

    public static void main(String[] args) throws Exception
    {
        new LoginFrame().init();
    }
}

若是用户正常输入其用户名、密码,输入正确时能够正常登录,输入错误将提示输入失败。但若是这个用户是一个Cracker,能够输入'or true or',也会显示登录成功。运行的后台能够看到以下SQL语句

# 利用SQL注入后生成的SQL语句
select * from jdbc_test where jdbc_name = '' or true or '' and jdbc_desc = ''

若是换成使用PreparedStatement来执行验证,而不是直接使用Statement

private boolean validate(String userName, String userPass)
{
    try(
        Connection conn = DriverManager.getConnection(url
            , user ,pass);
        PreparedStatement pstmt = conn.prepareStatement(
            "select * from jdbc_test where jdbc_name=? and jdbc_desc=?"))
    {
        pstmt.setString(1, userName);
        pstmt.setString(2, userPass);
        try(
            ResultSet rs = pstmt.executeQuery())
        {
            //若是查询的ResultSet里有超过一条的记录,则登陆成功
            if (rs.next())
            {
                return true;
            }
        }
    }
    catch(Exception e)
    {
        e.printStackTrace();
    }
    return false;
}
  • PreparedStatement预编译SQL语句,性能更好

  • PreparedStatement无须“拼接”SQL字符串,编程更简单

  • 使用PreparedStatement可防止SQL注入,安全性更好

使用PreparedStatement执行带占位符参数的SQL语句时,SQL语句中的占位符参数只能代替普通值,不要使用占位符参数代替表名、列名等数据库对象,更不要用占位符参数来代替SQL语句中的insert、select等关键字

使用CallableStatement调用存储过程

MySQL数据库中建立一个简单的存储过程的SQL语句

delimiter //
create procedure add_pro(a int, b int ,out sum int)
begin
set sum = a + b;
end;
//

上面的SQL语句将MySQL的语句结束符改成双斜线(//),这样就能够在建立存储过程当中使用分号做为分隔符(默认使用分号做为语句结束符)程序建立了名为add_pro的存储过程,该存储过程包含三个参数:a、b是传入参数,而sum使用out修饰,是传出参数

调用存储过程使用CallableStatement,经过Connection的prepareCall()方法来建立CallableStatement对象,建立该对象时须要传入调用存储过程的SQL语句。调用存储过程的SQL语句老是这种格式:{call 过程名(?,?,?...)},其中的问号做为存储过程参数的占位符

// 使用Connection来建立一个CallableStatement对象
cstmt = conn.prepareCall("{call add_pro(?,?,?)}");

存储过程的参数既有传入参数,也有传出参数,所谓传入参数就是Java程序必须为这些参数传入值,能够经过CallableStatement的setXxx()方法为传入参数设置值;所谓传出参数就是Java程序能够经过该参数获取存储过程里的值,CallableStatement须要调用registerOutParameter()方法来注册该参数

// 注册CallableStatement的第三个参数是int类型
cstmt.registerOutParameter(3, Types.INTEGER);

以后调用CallableStatement的execute()方法来执行存储过程,执行结束后经过CallableStatement对象的getXxx(int index)方法来获取指定传出参数的值

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
import java.io.*;
import java.sql.*;

public class CallableStatementTest
{
    private String driver;
    private String url;
    private String user;
    private String pass;
    public void initParam(String paramFile)throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
    }
    public void callProcedure()throws Exception
    {
        // 加载驱动
        Class.forName(driver);
        try(
            // 获取数据库链接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection来建立一个CallableStatment对象
            CallableStatement cstmt = conn.prepareCall(
                "{call add_pro(?,?,?)}"))
        {
            cstmt.setInt(1, 4);
            cstmt.setInt(2, 5);
            // 注册CallableStatement的第三个参数是int类型
            cstmt.registerOutParameter(3, Types.INTEGER);
            // 执行存储过程
            cstmt.execute();
            // 获取,并输出存储过程传出参数的值。
            System.out.println("执行结果是: " + cstmt.getInt(3));
        }
    }
    public static void main(String[] args) throws Exception
    {
        CallableStatementTest ct = new CallableStatementTest();
        ct.initParam("mysql.ini");
        ct.callProcedure();
    }
}

管理结果集

JDBC使用ResultSet来封装执行查询获得的查询结果,而后经过移动ResultSet的记录指针来取出结果集的内容。除此以外,JDBC还容许经过ResultSet来更新记录,并提供了ResultSetMetaData来得到ResultSet对象的相关信息

可滚动、可更新的结果集

可滚动的结果集:可使用absolute()、previous()、afterLast()等方法只有移动指针记录的ResultSet

以默认形式打开的ResultSet是不可更新的,若是但愿建立可更新的ResultSet,则必须在Connection在建立Statement或PreparedStatement时,传入额外的参数:

  • resultSetType:控制ResultSet的类型,该参数能够取以下三个值

    • ResultSet.TYPE_FORWARD_ONLY:该常量控制记录指针只能向前移动

    • ResultSet.TYPE_SCROLL_INSENSITIVE:该常量控制记录指针自由移动(可滚动结果集),但底层的数据改变不影响结果集ResultSet的内容

    • ResultSet.TYPE_SCROLL_SENSITIVE:该常量控制记录指针自由移动,但底层数据的影响会改变结果集ResultSet的内容

  • resultSetConcurrency:控制ResultSet的并发类型,该参数能够接收以下两个值

    • ResultSet.CONCUR_READ_ONLY:该常量表示ResultSet是只读并发模式(默认)

    • ResultSet.CONCUR_UPDATABLE:该常量表示ResultSet是更新并发模式

// 使用Connection建立一个PreparedStatement对象
// 传入控制结果集可滚动、可更新的参数
pstmt = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);

可更新的结果集还须要知足以下两个条件:

  • 全部数据都应该来自一个表

  • 选出的数据集必须包含主列键

经过该PreparedStatement建立的ResultSet就是可滚动的、可更新的,程序可调用的updateXxx(int columnIndex, Xxx value)方法来修改记录指针所指记录、特定列的值,最后调用ResultSet的updateRow()方法来提交修改

Java8为ResultSet添加了updateObject(String columnLabel, Object x, SQLType targetSqlType)和updateObject(int columnIndex, Object x, SQLType targetSqlType)两个默认方法,这两个方法能够直接用Object来修改记录指针所指记录、特定列的值,其中SQLType用于指定该数据列的类型

import java.util.*;
import java.io.*;
import java.sql.*;

public class ResultSetTest
{
    private String driver;
    private String url;
    private String user;
    private String pass;
    public void initParam(String paramFile)throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
    }
    public void query(String sql)throws Exception
    {
        // 加载驱动
        Class.forName(driver);
        try(
            // 获取数据库链接
            Connection conn = DriverManager.getConnection(url , user , pass);
            // 使用Connection来建立一个PreparedStatement对象
            // 传入控制结果集可滚动,可更新的参数。
            PreparedStatement pstmt = conn.prepareStatement(sql
                , ResultSet.TYPE_SCROLL_INSENSITIVE
                , ResultSet.CONCUR_UPDATABLE);
            ResultSet rs = pstmt.executeQuery())
        {
            rs.last();
            int rowCount = rs.getRow();
            for (int i = rowCount; i > 0 ; i-- )
            {
                rs.absolute(i);
                System.out.println(rs.getString(1) + "\t"
                    + rs.getString(2) + "\t" + rs.getString(3));
                // 修改记录指针全部记录、第2列的值
                rs.updateString(2 , "学生名" + i);
                // 提交修改
                rs.updateRow();
            }
        }
    }
    public static void main(String[] args) throws Exception
    {
        ResultSetTest rt = new ResultSetTest();
        rt.initParam("mysql.ini");
        rt.query("select * from student_table");
    }
}

处理Blob类型数据

Blob——Binary long Object——二进制长对象,Blob列一般用于存储大文件,典型的Blob内容是一张图片或者一个声音文件,因为他们的特殊性,必须使用特殊的方式来存储。使用Blob列能够把照片声音等文件的二进制数据保存在数据库里,并能够从数据库里恢复指定文件

若是须要将图片插入数据库,显然不能经过普通的SQL语句来完成,由于有一个关键的问题,Blob常量没法表示,因此将Blob数据插入数据库须要使用PreparedStatement。该对象有一个方法:setBinaryStream(int parameterIndex, InputStream x)该方法能够为指定参数传入二进制流,从而能够实现将Blob数据保存到数据库的功能

当须要从ResultSet里取出Blob数据时,能够调用ResultSet的getBlob(int columnIndex)方法,该方法将返回一个Blob对象Blob对象提供了getBinaryStream()方法获取该获取该Blob数据的输入流,也可使用Blob对象的getBytes()方法直接取出该Blob对象封装的二进制数据

为了把图片放入数据库,本程序先使用以下SQL语句来创建一个数据表:

create table img_table 
{ 
img_id int auto_increment primary key, 
img_name varchar(255), 
# 建立一个mediumblob类型的数据列,用于保存图片数据 
ima_data mediumblob 
};

img_data列使用mediumblob类型,而不是blob类型。由于MySQL数据库里的blob类型最多只能存储64kb的内容,因此使用mediumblob类型,该类型能够存储16M内容

// ---------将指定图片放入数据库---------
public void upload(String fileName)
{
    // 截取文件名
    String imageName = fileName.substring(fileName.lastIndexOf('\\') + 1, fileName.lastIndexOf('.'));
    File f = new File(fileName);
    try(
        InputStream is = new FileInputStream(f)
        )
    {
        // 设置图片名参数
        insert.setString(1, imageName);
        // 设置二进制流参数
        insert.setBinaryStream(2, is, (int)f.length());
        int affect = insert.executeUpdate();
        if (affect == 1)
        {
            // 从新更新ListModel,将会让JList显示最新的图片列表
            fillListModel();
        }
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }
}
// ---------根据图片ID来显示图片----------
public void showImage(int id)throws SQLException
{
    // 设置参数
    query.setInt(1, id);
    try(
        // 执行查询
        ResultSet rs = query.executeQuery()
        )
    {
        if (rs.next())
        {
            // 取出Blob列
            Blob imgBlob = rs.getBlob(1);
            // 取出Blob列里的数据
            ImageIcon icon=new ImageIcon(imgBlob.getBytes(1L, (int)imgBlob.length()));
            imageLabel.setIcon(icon);
        }
    }
}

使用resultsetmetaData分析结果集

描述ResultSet信息的数据——ResultSetMetaData

MetaData即元数据,即描述其它数据的数据,所以ResultSetMetaData封装了描述ResultSet对象的数据

ResultSet的getMetaData()方法返回该ResultSet对应的ResultSetMetaData对象,就可经过ResultSetMetaData提供的大量方法返回ResultSet的描述信息

  • int getColumnCount():返回该ResultSet的列数量

  • String getColumnName(int column):返回指定索引的列名

  • int getColumnType(int column):返回指定索引的列类型

Java7的RowSet1.1

RowSet接口继承了ResultSet接口,RowSet接口下包含JdbcRowSet、CachedRowSet、FilteredRowSet、JoinRowSet和WebRowSet经常使用子接口。除了JdbcRowSet须要保持与数据库的链接以外,其他4个子接口都是离线的RowSet,无须保持与数据库的链接

RowSet默认是一个可滚动,可更新,可序列化的结果集,并且它做为JavaBeans,能够方便地在网络间传输,用于两端的数据同步。对于离线RowSet而言,程序在建立RowSet时已把数据从底层数据库读取到了内存,所以能够充分利用计算机的内存,从而下降数据库服务器的负载,提供程序性能

RowSet规范的接口类图
clipboard.png

Java7新增的RowSetFactory与RowSet

RowSet接口中定义的经常使用方法:

  • setUrl(String url):设置该RowSet要访问的数据库的URL

  • setUsername(String name):设置该RowSet要访问的数据库的用户名

  • setPassword(String password):设置该RowSet要访问的数据库的密码

  • setCommand(String sql):设置使用该sql语句的查询结果来装填该RowSet

  • execute():执行查询

  • populate(ResultSet rs):让该RowSet直接包装给定的ResultSet对象

Java7新增了RowSetProvider类和RowSetFactory接口,其中RowSetProvider负载建立RowSetFactory,而RowSetFactory则提供了以下方法来建立RowSet实例:

  • CachedRowSet createCachedRowSet():建立一个默认的CachedRowSet

  • FilteredRowSet createFilteredRowSet():建立一个默认的FilteredRowSet

  • JoinRowSet createJoinRowSet():建立一个默认的JoinRowSet

  • WebRowSet createWebRowSet():建立一个默认的WebRowSet

  • JdbcRowSet createJdbcRowSet():建立一个默认的JdbcRowSet

提供使用RowSetFactory,就能够把应用程序与RowSet实现类分离开,避免直接使用JdbcRow SetImpl等非公开的API,也更有利于后期的升级、扩展

import java.util.*;
import java.io.*;
import java.sql.*;
import javax.sql.rowset.*;

public class RowSetFactoryTest
{
    private String driver;
    private String url;
    private String user;
    private String pass;
    public void initParam(String paramFile) throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
    }

    public void update(String sql)throws Exception
    {
        // 加载驱动
        Class.forName(driver);
        // 使用RowSetProvider建立RowSetFactory
        RowSetFactory factory = RowSetProvider.newFactory();
        try(
            // 使用RowSetFactory建立默认的JdbcRowSet实例
            JdbcRowSet jdbcRs = factory.createJdbcRowSet()
            )
        {
            // 设置必要的链接信息
            jdbcRs.setUrl(url);
            jdbcRs.setUsername(user);
            jdbcRs.setPassword(pass);
            // 设置SQL查询语句
            jdbcRs.setCommand(sql);
            // 执行查询
            jdbcRs.execute();
            jdbcRs.afterLast();
            // 向前滚动结果集
            while (jdbcRs.previous())
            {
                System.out.println(jdbcRs.getString(1)
                    + "\t" + jdbcRs.getString(2)
                    + "\t" + jdbcRs.getString(3));
                if (jdbcRs.getInt("student_id") == 3)
                {
                    // 修改指定记录行
                    jdbcRs.updateString("student_name", "源博雅");
                    jdbcRs.updateRow();
                }
            }
        }
    }
    public static void main(String[] args)throws Exception
    {
        RowSetFactoryTest jt = new RowSetFactoryTest();
        jt.initParam("mysql.ini");
        jt.update("select * from student_table");
    }
}

离线RowSet

离线RowSet会直接将底层数据读入内存中,封装成RowSet对象,而RowSet对象则彻底能够当成Java Bean来使用。所以不只安全,并且编程简单。CachedRowSet是全部离线RowSet的父接口

以下程序①处调用了RowSet的populate(ResultSet rs)方法来包装给的的ResultSet,接着关闭了ResultSet、Statement、Connection等数据库资源。若是程序直接返回ResultSet,那么这个Result没法使用——由于底层的Connection已经关闭;但程序返回的是CachedRowSet,一个离线RowSet,所以程序依然能够读取、修改RowSet中的记录

为了将程序对离线RowSet所作的修改同步到底层数据库,程序在调用RowSet的acceptChanges()方法时必须传入Connection

public class CachedRowSetTest
{
    private static String driver;
    private static String url;
    private static String user;
    private static String pass;
    public void initParam(String paramFile) throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
    }

    public CachedRowSet query(String sql) throws Exception
    {
        // 加载驱动
        Class.forName(driver);
        // 获取数据库链接
        Connection conn = DriverManager.getConnection(url, user, pass);
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(sql);
        // 使用RowSetProvider建立RowSetFactory
        RowSetFactory factory = RowSetProvider.newFactory();
        // 建立默认的CachedRowSet实例
        CachedRowSet cachedRs = factory.createCachedRowSet();
        // 使用ResultSet装填RowSet
        cachedRs.populate(rs);    // ①
        // 关闭资源
        rs.close();
        stmt.close();
        conn.close();
        return cachedRs;
    }
    public static void main(String[] args)throws Exception
    {
        CachedRowSetTest ct = new CachedRowSetTest();
        ct.initParam("mysql.ini");
        CachedRowSet rs = ct.query("select * from student_table");
        rs.afterLast();
        // 向前滚动结果集
        while (rs.previous())
        {
            System.out.println(rs.getString(1)
                + "\t" + rs.getString(2)
                + "\t" + rs.getString(3));
            if (rs.getInt("student_id") == 3)
            {
                // 修改指定记录行
                rs.updateString("student_name", "安倍晴明");
                rs.updateRow();
            }
        }
        // 从新获取数据库链接
        Connection conn = DriverManager.getConnection(url, user, pass);
        conn.setAutoCommit(false);
        // 把对RowSet所作的修改同步到底层数据库
        rs.acceptChanges(conn);
    }
}

离线RowSet的查询分页

CachedRowSet的分页功能:一次只装载ResultSet里的某几条记录,这样就能够避免CachedRowSet占用内存过大的问题

CachedRowSet提供了以下方法来控制分页:

  • populate(ResultSet rs, int startRow):使用给定的Result装填RowSet,从ResultSet的第startRow条记录但是装填

  • setPageSize(int pageSize):设置CachedRowSet每次返回记录条数

  • previousPage():在底层ResultSet可用状况下,让CachedRowSet读取上一页记录

  • nextPage():在底层ResultSet可用状况下,让CachedRowSet读取下一页记录

    public class CachedRowSetPage
    {

    private String driver;
    private String url;
    private String user;
    private String pass;
    public void initParam(String paramFile)throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
    }
    
    public CachedRowSet query(String sql, int pageSize, int page) throws Exception
    {
        // 加载驱动
        Class.forName(driver);
        try(
            // 获取数据库链接
            Connection conn = DriverManager.getConnection(url , user , pass);
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(sql)
            )
        {
            // 使用RowSetProvider建立RowSetFactory
            RowSetFactory factory = RowSetProvider.newFactory();
            // 建立默认的CachedRowSet实例
            CachedRowSet cachedRs = factory.createCachedRowSet();
            // 设置每页显示pageSize条记录
            cachedRs.setPageSize(pageSize);
            // 使用ResultSet装填RowSet,设置从第几条记录开始
            cachedRs.populate(rs, (page - 1) * pageSize + 1);
            return cachedRs;
        }
    }
    public static void main(String[] args)throws Exception
    {
        CachedRowSetPage cp = new CachedRowSetPage();
        cp.initParam("mysql.ini");
        CachedRowSet rs = cp.query("select * from student_table", 3, 2);   // ①
        // 向后滚动结果集
        while (rs.next())
        {
            System.out.println(rs.getString(1)
                + "\t" + rs.getString(2)
                + "\t" + rs.getString(3));
        }
    }

    }

事务处理

事务的概念和MySQL事务支持

事务是由一步或几步数据库操做序列组成的逻辑执行单元,这系列操做要么所有执行,要么所有放弃执行。

事务具备四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持续性(Durability)。这四个特性也简称ACID性

  • 原子性:事务是应用中最小的执行单位,就如原子是天然界最小颗粒,具备不可再分的特征同样。事务是应用中不可再分的最小逻辑执行体

  • 一致性:事务执行的结果,必须使数据库从一个一致性状态,变到另外一个一致性状态。当数据库中只包含事务成功提交的结果时,数据库处于一致性状态。一致性是经过原子性来保证的

  • 隔离性:各个事务的执行互不干扰,任意一个事务的内部操做对其余并发的事务,都是隔离的。也就是说:并发执行的事务之间不能看到对方的中间状态,并发执行的事务之间不能相互影响

  • 持续性:持续性也称为持久性,指事务一旦提交,对数据所作的任何改变,都要记录到永久存储器中,一般是保存进物理数据库

数据库的事务有下列语句组成:

  • 一组DML(Data Manipulate Language,即数据操做语言),通过这组DML修改后数据将保持较好的一致性

  • 一个DDL(Data Definition Language,即数据定义语言)语句

  • 一个DCL(Data control Language,即数据控制语言)语句

DDL和DCL语句最多只能有一个,由于DDL和DCL语句都会致使事务当即提交

当事务所包含的所有数据库操做都成功执行后,应该提交(commit)事务,使这些修改永久生效。事务提交有两种方式:显式提交和自动提交

  • 显式提交:使用commit

  • 自动提交:执行DDL或DCL,或者程序正常退出

当事务所包含的任意一个数据库操做执行失败后,应该回滚(rollback)事务,使该事务中所作的修改所有失效。事务回滚的方式有两种:显式回滚和自动回滚

  • 显式回滚:使用rollback关键字

  • 隐式回滚:系统错误或者强行退出

MySQL默认关闭事务(即打开自动提交事务),在默认状况下,在MySQL控制台输入一条DML语句,该语句会马上保存到数据库中。可使用下面的语句来开启事务(即关闭自动提交事务):

// 关闭自动提交,即开启事务
set autocommit = 0; 
// 开启自动提交,即关闭事务
set autocommit = 1;

调用 set autocommit = 0; 命令后,该命令行窗口里的全部DML语句都不会当即生效,上一个事务结束后第一条DML语句将开始一个新的事务,然后续执行的全部SQL语句都处于该事务中。除非使用commit提交事务、或正常退出、或运行DDL语句或DCL语句致使事务隐式提交。也可使用rollback回滚来结束事务,使用rollback结束事务将会使此事务中的DML语句所作的修改所有失效

一个MySQL命令行窗口表明一个Session,在该窗口里设置set autocommit = 0; 至关于关闭了该链接Session的自动提交,对其余链接不会有任何影响

若是不想使得整个Session都打开事务,可使用start transaction或begin这两个命令,它们都表示临时性地开始一次事务。处于start transaction或begin后的DML语句不会当即生效,除非使用commit显式提交事务,或者使用DDL语句或DCL语句隐式提交事务

以下SQL将不会对数据库有任何影响

# 临时开始事务
begin;
# 向player_table表插入3条数据
insert into player_table
values(null, 'Westbrook', 1);
insert into player_table
values(null, 'Harden', 2); 
insert into player_table
values(null, 'Durant', 3); 
# 查询player_table表的记录
select * from player_table;    # ①
# 回滚事务
rollback;
# 再次查询
select * from player_table;    # ②

经过使用savepoint设置事务的中间点可让事务回滚到指定中间点,而不是回滚所有事务。普通的提交、回滚都会结束当前事务,但回滚到指定中间点由于依然处于事务之中,因此不会结束当前事务

savepoint a;
# 回滚到指定中间点
rollback to a;

JDBC的事务支持

JDBC链接的事务支持由Connection提供,Connection默认打开自动提交,即关闭事务,在这种状况下,每条SQL语句一旦执行,便会当即提交到数据库,永久生效,没法对其进行回滚操做

能够调用Connection的setAutoCommit()方法来关闭自动提交,开启事务

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

一旦事务开始以后,程序能够像日常同样建立Statement对象,建立了Statement对象以后,能够执行任意多条DML语句,这些SQL语句虽然被执行了,但这些SQL语句所做的修改不会生效,由于事务尚未结束。若是全部SQL语句执行成功,程序能够调用Connection的commit方法来提交事务

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

若是任意一条SQL语句执行失败,应该用Connection的rollback来回滚事务

//回滚事务
conn.rollback();

当Connection遇到一个未处理的SQLException异常时,系统将会非正常退出,事务也会自动回滚。但若是程序捕获了该异常,则须要在异常处理块中显式地回滚事务

Connection设置中间点的方法:

  • Savepoint setSavepoint():在当前事务中建立一个未命名的中间点,并返回表明该中间点的Savepoint对象

  • Savepoint setSavepoint(String name):在当前事务中建立一个具备指定名称的中间点,并返回表明该中间点的Savepoint对象

一般来讲,设置中间点时没有太大的必要指定名称,由于Connection回滚到指定中间点时,并非根据名字回滚的,而是根据中间点对象回滚的。Connection提供了rollback(Savepoint savepoint)方法来回滚到指定中间点

Java8加强的批量更新

批量更新必须获得底层数据库的支持,可经过调用DatabaseMetaData的supportsBatchUpdates()方法来查看底层数据库是否支持批量更新

批量更新须要先建立一个Statement对象,而后利用该对象的addBatch()方法将多条SQL语句同时收集起来,最后调用Statement对象的executeBatch()(或executeLargeBatch())方法同时执行这些SQL语句

批量更新代码:

Statement stmt = conn.createStatement();  
//使用Statement同时收集多个SQL语句  
stmt.addBatch(sql1);  
stmt.addBatch(sql2);  
stmt.addBatch(sql3);  
...  
//同时执行全部的SQL语句  
stmt.executeBatch();

为了让批量操做能够正确地处理错误,必须把批量执行的操做视为单个事务,若是批量更新在执行过程当中失败,则让事务回滚到批量操做开始以前的状态。程序应该在开始批量操做以前先关闭自动提交,而后开始收集更新语句,当批量操做结束以后,提交事务,并恢复以前的自动提交模式

//保存当前的自动的提交模式  
Boolean autoCommit = conn.getAutoCommit();  
//关闭自动提交  
conn.setAutoCommit(false);  
Statement stmt = conn.createStatement();  
//使用Statement同时收集多条SQL语句  
stmt.addBatch(sql1);  
stmt.addBatch(sql2);  
stmt.addBatch(sql3);  
...  
//同时提交全部的SQL语句  
stmt.executeBatch();  
//提交修改  
conn.commit();  
//恢复原有的自动提交模式  
conn.setAutoCommit(autoCommit);

分析数据库信息

使用DatabaseMetaData分析数据库信息

JDBC提供了DatabaseMetaData来封装数据库链接对应数据库的信息,经过Connection提供的getMetaData()方法就能够获取数据库对应的DatabaseMetaData对象

DatabaseMetaData接口一般由驱动程序供应商提供实现,其目的是让用户了解底层数据库的相关信息。使用该接口的目的是发现如何处理底层数据库,尤为是对于试图与多个数据库一块儿使用的应用程序

许多DatabaseMetaData方法以ResultSet对象的形式返回查询信息,而后使用ResultSet的常规方法(如getString()和getInt())便可从这些ResultSet对象中获取数据。若是查询的信息不可用,则将返回一个空ResultSet对象

DatabaseMetaData的不少方法都须要传入一个xxxPattern模式字符串,这里的xxxPattern不是正则表达式,而是SQL里的模式字符串,即用百分号(%)表明任意多个字符,使用下划线(_)表明一个字符。在一般状况下,若是把该模式字符串的参数值设置为null,即代表该参数不做为过滤条件

import java.sql.*;  
import java.io.*;  
import java.util.*;  
public class DatabaseMetaDataTest{  
    private String driver;  
    private String url;  
    private String user;  
    private String pass;  
    public void initParam(String paramFile) throws Exception{  
        //使用Properties类来加载属性文件  
        Properties props = new Properties();  
        props.load(new FileInputStream(paramFile));  
        driver = props.getProperty("driver");  
        url = props.getProperty("url");  
        user = props.getProperty("user");  
        pass = props.getProperty("pass");  
    }  
    public void info() throws Exception{  
        //加载驱动  
        Class.forName(driver);  
        try(  
            //获取数据库链接  
            Connection conn = DriverManager.getConnection(url, user, pass);  
        ){  
            //获取DatabaseMetaData对象  
            DatabaseMetaData dbmd = conn.getMetaData();  
            //获取MySQL支持的全部表类型  
            ResultSet rs = dbmd.getTableTypes();  
            System.out.println("---MySQL支持的表类型信息---");  
            printResultSet(rs);  
            //获取当前数据库的所有数据表  
            rs = dbmd.getTables(null, null, "%", new String[]{"TABLE"});  
            System.out.println("---当前数据库里的数据表信息---");  
            printResultSet(rs);  
            //获取student_table表的主键  
            rs = dbmd.getPrimaryKeys(null, null, "student_table");  
            System.out.println("---student_table表的主键信息---");  
            printResultSet(rs);  
            //获取当前数据库的所有存储过程  
            rs = dbmd.getProcedures(null, null, "%");  
            System.out.println("---当前数据库里的存储过程信息---");  
            printResultSet(rs);  
            //获取teacher_table表和student_table表之间的外键约束  
            rs = dbmd.getCrossReference(null, null, "teacher_table", null, null, "student_table");  
            System.out.println("---teacher_table表和student_table表之间的外键约束---");  
            printResultSet(rs);  
            //获取student_table表的所有数据列  
            rs = dbmd.getColumns(null, null, "student_table", "%");  
            System.out.println("---student_table表的所有数据列---");  
            printResultSet(rs);  
        }  
    }  
    public void printResultSet(ResultSet rs) throws SQLException{  
        ResultSetMetaData rsmd = rs.getMetaData();  
        //打印ResultSet的全部列标题  
        for(int i = 0; i < rsmd.getColumnCount(); i++){  
            System.out.print(rsmd.getColumnName(i + 1) + "\t");  
        }  
        System.out.print("\n");  
        //打印ResultSet的所有数据  
        while(rs.next()){  
            for(int i = 0; i < rsmd.getColumnCount(); i ++){  
                System.out.print(rs.getString(i + 1) + "\t");  
            }  
            System.out.print("\n");  
        }  
        rs.close();  
    }  
    public static void main(String args[]) throws Exception{  
        DatabaseMetaDataTest dmdt = new DatabaseMetaDataTest();  
        dmdt.initParam("sql.ini");  
        dmdt.info();  
    }  
}

使用系统表分析数据库信息

如已肯定应用程序所使用的数据库系统,则能够经过数据库的系统表来分析数据库信息。系统表又称为数据字典,数据字典的数据一般由数据库系统负责维护,用户一般只能查询数据字典,而不能修改数据字典的内容

MySQL数据库使用information_schema数据库来保存系统表,在该数据库里包含了大量系统表,经常使用系统表的简单介绍以下:

  • tables:存放数据库里全部数据表的信息

  • schemata:存放数据库里全部数据库(与MySQL的Schema对应)的信息

  • views:存放数据库里全部视图的信息

  • columns:存放数据库里全部列的信息

  • triggers:存放数据库里全部触发器的信息

  • routines:存放数据库里全部存储过程和函数的信息

  • key_column_usage:存放数据库里全部具备约束的键信息

  • table_constraints:存放数据库里所有约束的表信息

  • statistics:存放数据库里所有索引的信息

select * from schemata;  
select * from tables where table_schema = 'select_test';  
select * from columns where table_name = 'student_table';

选择合适的分析方式

一般而言,若是使用DatabaseMetaData来分析数据库信息,则具备更好的跨数据库特性,应用程序能够作到数据库无关;但可能没法准确获取数据库的更多细节

使用数据库系统表来分析数据库系统信息会更加准确,但使用系统表也有坏处——这种方式与底层数据库耦合严重,采用这种方式将会致使程序只能运行在特定的数据库之上

一般来讲,若是须要得到数据库信息,包括该数据库驱动提供了哪些功能,则应该利用DatabaseMetaData来了解该数据库支持哪些功能。彻底可能出现这样一种状况:对于底层数据库支持的功能,但数据库驱动没有提供该功能,程序仍是不能使用该功能。使用DatabaseMetaData则不会出现这种问题

若是须要纯粹地分析数据库的静态对象,例如分析数据库系统里包含多少数据库、数据表、视图、索引等信息,则利用系统会更加合适

使用链接池管理链接

数据库链接的创建以及关闭是极耗费系统资源的操做,在多层结构的应用环境中,这种资源的耗费对系统性能影响尤其明显。经过前面介绍的方式(经过DriverManager获取链接)获取的数据库链接,一个数据库链接对象均对应一个物理数据库链接,每次操做都打开一个物理链接,使用完后当即关闭链接。频繁地打开、关闭链接将形成系统性能低下

数据库链接池的解决方案是: 当应用程序启动时,系统主动创建足够的数据库链接,并将这些链接组成一个链接池。每次应用程序请求数据库链接时,无须从新打开链接,而是从链接池中取出已有的链接使用,使用完后再也不关闭数据库链接,而是直接将链接归还给链接池。经过使用链接池,将大大提供程序的运行效率

对于共享资源的恶状况,有一个通用的设计模式:资源池(Resource Pool),用于解决资源的频繁请求、释放所形成的性能降低。为了解决数据库链接的频繁请求,JDBC2.0规范引入了数据库链接池技术

数据库链接池是Connection对象的工厂。数据库链接池的经常使用参数以下:

  • 数据库的初始链接数

  • 链接池的最大链接数

  • 链接池的最小链接数

  • 链接池每次增长的容量

JDBC的数据库链接池使用javax.sql.DataSource来表示,DataSource只是一个接口,该接口一般由商用服务器(如WebLogic、WeSphere)等提供实现,也有一些开源组织提供实现(如DBCP和C3P0)

DataSource一般被称为数据源,它包含链接池和链接池管理两个部分,但习惯上咱们也常常把DataSource称为链接池

DBCP数据源

DBCP是Apache软件基金组织下的开源链接池实现,该链接池依赖该组织下的另外一个开源系统:common-pool。若是须要使用该链接池实现,则应在系统中增长以下两个jar文件:

  • commons-dbcp.jar:链接池的实现

  • commons-pool.jar:链接池实现的依赖库

Tomcat的链接池正是采用该链接池实现的。数据库链接池既能够与应用服务器整合使用,也能够由应用程序独立使用。下面的代码片断示范了使用DBCP来得到数据库链接的方式:

// 建立数据源对象  
BasicDataSource ds = new BasicDataSource();  
// 设置链接池所需的驱动  
ds.setDriverClassName("com.mysql.jdbc.Driver");  
// 设置链接数据库的URL  
ds.setUrl("jdbc:mysql://localhost:3306/javaee");  
// 设置链接数据库的用户名  
ds.setUsername("root");  
/ /设置链接数据库的密码  
ds.setPassword("pass");  
// 设置链接池的初始链接数  
ds.setInitialSize(5);  
// 设置链接池最多可有多少个活动链接数  
ds.setMaxActive(20);  
// 设置链接池中最少有2个空闲的链接  
ds.setMinIdle(2);

数据源和数据库链接不一样,数据源无须建立多个,它是产生数据库链接的工厂,所以整个应用只须要一个数据源便可。也就是说,对于一个应用,上面代码只要执行一次便可。建议把上面程序中的ds设置成static成员变量,而且在应用开始时当即初始化数据源对象,程序中全部须要获取数据库链接的地方直接访问该ds对象,并获取数据库链接便可

// 经过数据源获取数据库链接  
Connection conn = ds.getConnection();  
// 当数据库访问结束后,释放数据库链接  
conn.close();    // 上面代码并无关闭据库的物理链接
                 // 仅仅把数据库链接释放,归还给链接池
                 // 让其余客户端可使用该链接

C3P0数据源

C3P0数据源性能更胜一筹,Hibernate推荐使用该链接池。C3P0链接池不只能够自动清理再也不使用的Connection,还能够自动清理Statement和ResultSet

若是须要使用C3P0链接池,则应在系统中增长以下JAR文件

  • c3p0-0.9.1.2.jar: C3P0链接池的实现

    // 建立链接池实例  
    ComboPooledDataSource ds = new ComboPooledDataSource();  
    // 设置链接池链接数据库所需的驱动  
    ds.setDriverClass("com.mysql.jdbc.Driver");  
    // 设置链接数据库的url  
    ds.setJdbcUrl("jdbc:mysql://localhost:3306/javaee");  
    // 设置链接数据库的用户名、密码  
    ds.setUser("root");  
    ds.setPassword("root");  
    // 设置链接池的最大链接数  
    ds.setMaxPoolSize(40);  
    // 设置链接池的最小链接数  
    ds.setMinPoolSize(2);  
    // 设置链接池的初始链接数  
    ds.setInitialPoolSize(10);  
    // 设置链接池的缓存Statment的最大数  
    ds.setMaxStatements(180);  
    //得到数据库链接  
    Connection conn = ds.getConnection();
相关文章
相关标签/搜索