数据库 - Java JDBC事务管理和SavePoint

原文地址:https://www.journaldev.com/2483/java-jdbc-transaction-management-savepoint

当我们处理关系数据库时,需要使用Java中的事务管理。我们使用JDBC API进行数据库操作,今天我们将学习如何使用JDBC事务管理。在《 JDBC教程》中,我们学习了如何使用JDBC API进行数据库连接并执行SQL查询。我们还研究了不同类型的驱动程序以及如何编写松散耦合的JDBC程序,这些程序可以帮助我们轻松地从一个数据库服务器切换到另一个数据库服务器。

Java JDBC中的事务管理

本教程旨在提供有关JDBC事务管理以及使用JDBC Savepoint进行部分回滚的详细信息。

默认情况下,当我们创建数据库连接时,它以自动提交模式运行。这意味着只要我们执行查询并完成查询,提交就会自动触发。因此,我们触发的每个SQL查询都是一个事务,如果我们正在运行一些DML或DDL查询,则在每个SQL语句完成后,所做的更改都会保存到数据库中。

有时,我们希望将一组SQL查询作为事务的一部分,以便在所有查询正常运行时提交它们。如果出现任何异常,我们可以选择回滚作为事务一部分执行的所有查询。

让我们通过一个简单的示例来了解一下,我们希望利用JDBC事务管理支持来确保数据完整性。假设我们有UserDB数据库,并且Employee信息保存到两个表中。对于我的示例,我正在使用MySQL数据库,但是它将在其他关系数据库以及Oracle和PostgreSQL上正常运行。

这些表将员工信息和地址详细信息存储在表中,这些表的DDL脚本如下所示。

CREATE TABLE `Employee` (
  `empId` int(11) unsigned NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`empId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `Address` (
  `empId` int(11) unsigned NOT NULL,
  `address` varchar(20) DEFAULT NULL,
  `city` varchar(5) DEFAULT NULL,
  `country` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`empId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我们的最终项目如下图所示,我们将逐一研究每个类。

JDBC-Transaction-Management-Savepoint-Project.png

如您所见,在项目构建路径中有MySQL JDBC jar,因此我们可以连接到MySQL数据库。

DBConnection.java

package com.journaldev.jdbc.transaction;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DBConnection {

    public final static String DB_DRIVER_CLASS = "com.mysql.jdbc.Driver";
    public final static String DB_URL = "jdbc:mysql://localhost:3306/UserDB";
    public final static String DB_USERNAME = "pankaj";
    public final static String DB_PASSWORD = "pankaj123";

    public static Connection getConnection() throws ClassNotFoundException, SQLException {

        Connection con = null;

        // load the Driver Class
        Class.forName(DB_DRIVER_CLASS);

        // create the connection now
        con = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);

        System.out.println("DB Connection created successfully");
        return con;
    }
}

DBConnection是我们要创建供其他类使用的MySQL数据库连接的类。

EmployeeJDBCInsertExample.java

package com.journaldev.jdbc.transaction;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class EmployeeJDBCInsertExample {

    public static final String INSERT_EMPLOYEE_QUERY = "insert into Employee (empId, name) values (?,?)";

    public static final String INSERT_ADDRESS_QUERY = "insert into Address (empId, address, city, country) values (?,?,?,?)";

    public static void main(String[] args) {

        Connection con = null;
        try {
            con = DBConnection.getConnection();
            
            insertEmployeeData(con, 1, "Pankaj");

            insertAddressData(con, 1, "Albany Dr", "San Jose", "USA");
        } catch (SQLException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {

            try {
                if (con != null)
                    con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    public static void insertAddressData(Connection con, int id,
            String address, String city, String country) throws SQLException {
        PreparedStatement stmt = con.prepareStatement(INSERT_ADDRESS_QUERY);
        stmt.setInt(1, id);
        stmt.setString(2, address);
        stmt.setString(3, city);
        stmt.setString(4, country);

        stmt.executeUpdate();

        System.out.println("Address Data inserted successfully for ID=" + id);
        stmt.close();
    }

    public static void insertEmployeeData(Connection con, int id, String name)
            throws SQLException {
        PreparedStatement stmt = con.prepareStatement(INSERT_EMPLOYEE_QUERY);
        stmt.setInt(1, id);
        stmt.setString(2, name);

        stmt.executeUpdate();

        System.out.println("Employee Data inserted successfully for ID=" + id);
        stmt.close();
    }

}

这是一个简单的JDBC程序,我们将在上面创建的Employee和Address表中插入用户提供的值。

现在,当我们运行该程序时,将得到以下输出。

DB Connection created successfully
Employee Data inserted successfully for ID=1
com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'city' at row 1
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
    at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
    at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.insertAddressData(EmployeeJDBCInsertExample.java:45)
    at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.main(EmployeeJDBCInsertExample.java:23)

如您所见,当我们尝试将数据插入到地址表中时,由于该值大于列的大小,因此引发了SQLException。

如果查看Employee和Address表的内容,您会注意到Employee表中有数据,而Address表中没有数据。这将成为一个严重的问题,因为只有部分数据被正确插入,并且如果我们再次运行该程序,它将尝试再次插入Employee表并抛出以下异常。

com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Duplicate entry '1' for key 'PRIMARY'
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:931)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2941)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
    at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
    at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.insertEmployeeData(EmployeeJDBCInsertExample.java:57)
    at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.main(EmployeeJDBCInsertExample.java:21)

因此,我们现在无法为员工将数据保存到Address表中。因此,这个程序会导致数据完整性问题,这就是为什么我们需要事务管理来成功地插入两个表,或者在出现任何异常时回滚所有内容。

JDBC事务管理

JDBC API提供了setAutoCommit()方法,通过该方法我们可以禁用连接的自动提交功能。我们仅应在需要时禁用自动提交,因为除非我们在连接上调用commit()方法,否则不会提交事务。数据库服务器使用表锁来实现事务管理及其资源密集型过程。因此,我们应该在完成交易后立即提交交易。让我们编写另一个程序,在该程序中我们将使用JDBC事务管理功能来确保不违反数据完整性。

EmployeeJDBCTransactionExample.java

package com.journaldev.jdbc.transaction;

import java.sql.Connection;
import java.sql.SQLException;

public class EmployeeJDBCTransactionExample {

    public static void main(String[] args) {
        
        Connection con = null;
        try {
            con = DBConnection.getConnection();
            
            //set auto commit to false
            con.setAutoCommit(false);

            EmployeeJDBCInsertExample.insertEmployeeData(con, 1, "Pankaj");

            EmployeeJDBCInsertExample.insertAddressData(con, 1, "Albany Dr", "San Jose", "USA");
            
            //now commit transaction
            con.commit();
            
        } catch (SQLException e) {
            e.printStackTrace();
            try {
                con.rollback();
                System.out.println("JDBC Transaction rolled back successfully");
            } catch (SQLException e1) {
                System.out.println("SQLException in rollback"+e.getMessage());
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                if (con != null)
                    con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

}

在运行该程序之前,请确保删除先前插入的数据。当您运行该程序时,将得到以下输出。

DB Connection created successfully
Employee Data inserted successfully for ID=1
com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'city' at row 1
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
    at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
    at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.insertAddressData(EmployeeJDBCInsertExample.java:45)
    at com.journaldev.jdbc.transaction.EmployeeJDBCTransactionExample.main(EmployeeJDBCTransactionExample.java:19)
JDBC Transaction rolled back successfully

输出类似于先前的程序,但是如果您查看数据库表,则会注意到数据没有插入到Employee表中。现在,我们可以更改城市值,使其适合列,然后重新运行程序以将数据插入两个表中。请注意,只有在两个插入都执行良好且其中任何一个抛出异常时才提交连接,我们将回滚完整事务。

JDBC Savepoint

有时,一个事务可以是多个语句的组,我们希望回滚到事务中的特定点。JDBC Savepoint帮助我们在事务中创建检查点,我们可以回滚到那个特定的检查点。为事务创建的任何保存点都会自动释放,并且在提交事务或回滚整个事务时无效。将事务滚回保存点将自动释放并使在有问题的保存点之后创建的任何其他保存点无效。

假设我们有一个Logs表,我们要在其中记录成功保存员工信息的消息。但是,由于它仅用于记录日志,因此如果在插入“日志”表时出现任何异常,我们就不希望回滚整个事务。让我们看看如何通过JDBC保存点实现这一目标。

CREATE TABLE `Logs` (
  `id` int(3) unsigned NOT NULL AUTO_INCREMENT,
  `message` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

EmployeeJDBCSavePointExample.java

package com.journaldev.jdbc.transaction;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Savepoint;

public class EmployeeJDBCSavePointExample {

    public static final String INSERT_LOGS_QUERY = "insert into Logs (message) values (?)";

    public static void main(String[] args) {

        Connection con = null;
        Savepoint savepoint = null;
        try {
            con = DBConnection.getConnection();

            // set auto commit to false
            con.setAutoCommit(false);

            EmployeeJDBCInsertExample.insertEmployeeData(con, 2, "Pankaj");

            EmployeeJDBCInsertExample.insertAddressData(con, 2, "Albany Dr",
                    "SFO", "USA");

            // if code reached here, means main work is done successfully
            savepoint = con.setSavepoint("EmployeeSavePoint");

            insertLogData(con, 2);

            // now commit transaction
            con.commit();

        } catch (SQLException e) {
            e.printStackTrace();
            try {
                if (savepoint == null) {
                    // SQLException occurred in saving into Employee or Address tables
                    con.rollback();
                    System.out
                            .println("JDBC Transaction rolled back successfully");
                } else {
                    // exception occurred in inserting into Logs table
                    // we can ignore it by rollback to the savepoint
                    con.rollback(savepoint); 
                    
                    //lets commit now
                    con.commit();
                }
            } catch (SQLException e1) {
                System.out.println("SQLException in rollback" + e.getMessage());
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                if (con != null)
                    con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    private static void insertLogData(Connection con, int i)
            throws SQLException {
        PreparedStatement stmt = con.prepareStatement(INSERT_LOGS_QUERY);
        
        //message is very long, will throw SQLException
        stmt.setString(1, "Employee information saved successfully for ID" + i);

        stmt.executeUpdate();
        System.out.println("Logs Data inserted successfully for ID=" + i);

        stmt.close();
    }

}

这个程序很容易理解。可以看到,在数据成功插入Employee和Address表之后,我正在创建保存点。如果出现了SQLException,并且savepoint为null,这意味着在执行针对Employee或Address表的insert查询时,会引发异常,因此我将回滚整个事务。

如果savepoint不为null,则意味着在将数据插入Logs表中时将出现SQLException,因此我仅将事务回滚到savepoint并提交。

如果您将运行上面的程序,则将看到以下输出。

DB Connection created successfully
Employee Data inserted successfully for ID=2
Address Data inserted successfully for ID=2
com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'message' at row 1
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
    at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
    at com.journaldev.jdbc.transaction.EmployeeJDBCSavePointExample.insertLogData(EmployeeJDBCSavePointExample.java:73)
    at com.journaldev.jdbc.transaction.EmployeeJDBCSavePointExample.main(EmployeeJDBCSavePointExample.java:30)

如果检查数据库表,您将注意到数据已成功插入Employee和Address表中。注意,当数据成功插入Employee和Address表并使用另一个事务插入日志表时,我们可以通过提交事务轻松地实现这一点。这只是一个展示在java程序中使用JDBC保存点的示例。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容