资讯专栏INFORMATION COLUMN

Java JDBC编程

邹强 / 2534人阅读

摘要:和很像,严格来说,应该是模仿了是设计。程序中使用该类的主要功能是获取对象,该类包含如下方法该方法获得对应数据库的连接代表数据库连接对象,每个代表一个物理连接会话。当执行查询时,返回查询到的结果集。程序可以通过操作该对象来取出查询结果。

JDBC基础

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

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

JDBC简介

Java语言的各种跨平台特性,都采用相似的结构。因为他们都需要让相同的程序在不同的平台上运行,所以需要中间的转换程序(为了实现Java程序的跨平台,Java为不同的操作系统提供了不同的Java虚拟机)。同样,为了JDBC程序可以跨平台,也需要不同的数据库厂商提供相应的驱动程序

Sun提供的JDBC可以完成以下三个基本操作:

建立与数据库的链接

执行SQL语句

获得SQL语句的执行结果

JDBC驱动程序

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

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) + "	"
                    + rs.getString(2) + "	"
                    + rs.getString(3) + "	"
                    + 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) + "	");
                        }
                        System.out.print("
");
                    }
                }
            }
            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) + "	"
                    + rs.getString(2) + "	" + 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规范的接口类图

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)
                    + "	" + jdbcRs.getString(2)
                    + "	" + 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)
                + "	" + rs.getString(2)
                + "	" + 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)
            + "	" + rs.getString(2)
            + "	" + 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) + "	");  
        }  
        System.out.print("
");  
        //打印ResultSet的全部数据  
        while(rs.next()){  
            for(int i = 0; i < rsmd.getColumnCount(); i ++){  
                System.out.print(rs.getString(i + 1) + "	");  
            }  
            System.out.print("
");  
        }  
        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则不会出现这种问题

如果需要纯粹地分析数据库的静态对象,例如分析数据库系统里包含多少数据库、数据

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66469.html

相关文章

  • JDBC系列】从源码角度理解JDBC和Mysql的预编译特性

    摘要:我们对语句做适当改变,就完成了注入,因为普通的不会对做任何处理,该例中单引号后的生效,拉出了所有数据。查询资料后,发现还要开启一个参数,让端缓存,缓存是级别的。结论是个好东西。 背景 最近因为工作调整的关系,都在和数据库打交道,增加了许多和JDBC亲密接触的机会,其实我们用的是Mybatis啦。知其然,知其所以然,是我们工程师童鞋们应该追求的事情,能够帮助你更好的理解这个技术,面对问题...

    longshengwang 评论0 收藏0
  • 几个数据持久化框架Hibernate、JPA、Mybatis、JOOQ和JDBC Template的

    摘要:不管是还是,表之间的连接查询,被映射为实体类之间的关联关系,这样,如果两个实体类之间没有实现关联关系,你就不能把两个实体或者表起来查询。 因为项目需要选择数据持久化框架,看了一下主要几个流行的和不流行的框架,对于复杂业务系统,最终的结论是,JOOQ是总体上最好的,可惜不是完全免费,最终选择JDBC Template。 Hibernate和Mybatis是使用最多的两个主流框架,而JOO...

    xietao3 评论0 收藏0
  • Java编程需要注意的地方

    摘要:学编程真的不是一件容易的事不管你多喜欢或是多会编程,在学习和解决问题上总会碰到障碍。熟练掌握核心内容,特别是和多线程初步具备面向对象设计和编程的能力掌握基本的优化策略。   学Java编程真的不是一件容易的事,不管你多喜欢或是多会Java编程,在学习和解决问题上总会碰到障碍。工作的时间越久就越能明白这个道理。不过这倒是一个让人进步的机会,因为你要一直不断的学习才能很好的解决你面前的难题...

    leanxi 评论0 收藏0
  • Java学习】JDBC的学习(了解CLass等)

    摘要:同时也有一些儿高级的处理,比如批处理更新事务隔离和可滚动结果集等。连接对象表示通信上下文,即,与数据库中的所有的通信是通过此唯一的连接对象。因为是针对类的关系而言,所以一个对象对应多个类的实例化。返回表示查询返回表示其它操作。 JDBC是什么? JDBC是一个Java API,用中文可以通俗的解释为,使用Java语言访问访问数据库的一套接口集合。这是调用者(程序员)和实行者(数据库厂商...

    cjie 评论0 收藏0
  • Java编程基础33——JDBC

    摘要:是访问数据库的标准规范提供了一种基准据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。在将此值发送到数据库时,驱动程序将它转换成一个类型值。 1.JDBC概念和数据库驱动程序 A: JDBC概述 JDBC(Java Data Base Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,...

    KitorinZero 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<