You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Oracle中未提交主表时如何插入主从数据?(JavaFX+11g XE)

解决Oracle主从表插入时的事务回滚问题

我明白你现在的困扰:插入主表(部门)后必须先提交才能拿到主键去插子表(员工),但这样一旦后续操作出错,就没法回滚已经提交的主表数据,只能手动删除,这完全不符合事务的原子性要求。其实根本不用提前提交——Oracle JDBC支持在不提交事务的情况下获取生成的主键,只要保证整个操作在同一个连接、同一个事务内执行就行。

先看看你代码里的核心问题:

  1. 你的Database类每次调用setconnection()都会重新创建连接,导致插入部门和员工用的是不同连接,不在同一个事务里,这才逼得你必须提前提交拿主键;
  2. 构造函数里用线程初始化连接的方式很容易导致连接未就绪就被调用,引发问题。

下面是修改后的完整解决方案,我会把关键改进点标注出来:

修改后的Database类(连接与事务管理)

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Database {
    private static final String driver = "oracle.jdbc.driver.OracleDriver";
    private static final String username = "HR";
    private static final String password = "hr";
    private static final String thinConn = "jdbc:oracle:thin:@localhost:1521:xe";
    
    // 单例实例,线程安全
    private static volatile Database instance;
    private Connection conn;

    // 私有构造,确保只能通过getInstance获取实例
    private Database() {
        try {
            Class.forName(driver);
            // 初始化唯一连接,关闭自动提交开启事务
            conn = DriverManager.getConnection(thinConn, username, password);
            conn.setAutoCommit(false);
            System.out.println("数据库连接已建立");
        } catch (SQLException | ClassNotFoundException ex) {
            Logger.getLogger(Database.class.getName()).log(Level.SEVERE, "初始化连接失败", ex);
        }
    }

    // 线程安全的单例获取方法
    public static Database getInstance() {
        if (instance == null) {
            synchronized (Database.class) {
                if (instance == null) {
                    instance = new Database();
                }
            }
        }
        return instance;
    }

    // 提供获取当前连接的方法,确保所有操作共用同一个连接
    public Connection getConnection() {
        return conn;
    }

    public ResultSet execQuery(PreparedStatement stmnt) throws SQLException {
        return stmnt.executeQuery();
    }

    public ResultSet execInsert(PreparedStatement stmnt) throws SQLException {
        stmnt.executeUpdate();
        ResultSet generatedKeys = stmnt.getGeneratedKeys();
        System.out.println("插入语句执行完成");
        return generatedKeys;
    }

    public void execUpdate(PreparedStatement stmnt) throws SQLException {
        stmnt.executeUpdate();
        System.out.println("更新语句执行完成");
        stmnt.close();
    }

    public void commit() throws SQLException {
        conn.commit();
        System.out.println("事务已提交");
    }

    public void rollback() throws SQLException {
        conn.rollback();
        System.out.println("事务已回滚");
    }

    public void rollback(Savepoint savepoint) throws SQLException {
        conn.rollback(savepoint);
        System.out.println("已回滚到指定保存点");
    }

    public Savepoint setSavepoint() throws SQLException {
        return conn.setSavepoint();
    }

    public void releaseSavepoint(Savepoint savepoint) throws SQLException {
        conn.releaseSavepoint(savepoint);
    }

    // 应用退出时关闭连接
    public void closeConnection() throws SQLException {
        if (conn != null && !conn.isClosed()) {
            conn.close();
            System.out.println("数据库连接已关闭");
        }
    }
}

修改后的InsertRollBackApp类(主从插入事务控制)

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class InsertRollBackApp extends Application {

    @Override
    public void start(Stage primaryStage) {
        Button btn = new Button();
        btn.setText("插入部门与员工");
        btn.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                Database db = Database.getInstance();
                try {
                    // 1. 插入部门(不提交,直接拿主键)
                    int deptId = insertDept(db);
                    // 2. 用拿到的主键插入员工
                    insertEmp(db, deptId);
                    // 3. 所有操作成功后统一提交
                    db.commit();
                    System.out.println("部门与员工插入成功");
                } catch (SQLException ex) {
                    Logger.getLogger(InsertRollBackApp.class.getName()).log(Level.SEVERE, null, ex);
                    System.out.println("出错了:" + ex.getMessage());
                    // 出错时回滚整个事务,包括部门和员工的插入
                    try {
                        db.rollback();
                        System.out.println("事务已回滚,没有留下脏数据");
                    } catch (SQLException rollbackEx) {
                        Logger.getLogger(InsertRollBackApp.class.getName()).log(Level.SEVERE, "回滚失败", rollbackEx);
                    }
                }
            }
        });

        StackPane root = new StackPane();
        root.getChildren().add(btn);
        Scene scene = new Scene(root, 300, 250);

        primaryStage.setTitle("事务控制演示");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public int insertDept(Database db) throws SQLException {
        String sql = "INSERT INTO DEPARTMENTS(DEPARTMENT_NAME) VALUES(?)";
        // 使用同一个连接创建PreparedStatement
        PreparedStatement stmnt = db.getConnection().prepareStatement(sql, new String[]{"DEPARTMENT_ID"});
        stmnt.setString(1, "DUMMY_DEPT");
        ResultSet rs = db.execInsert(stmnt);
        
        int deptId = 0;
        if (rs.next()) {
            deptId = rs.getInt(1);
        }
        // 关闭资源,避免泄漏
        rs.close();
        stmnt.close();
        return deptId;
    }

    public void insertEmp(Database db, int deptId) throws SQLException {
        String sql = "INSERT INTO EMPLOYEES(FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, DEPARTMENT_ID) "
                + "VALUES(?, ?, ?, ?, SYSDATE, ?, ?)";
        PreparedStatement stmnt = db.getConnection().prepareStatement(sql, new String[]{"EMPLOYEE_ID"});
        stmnt.setString(1, "M");
        stmnt.setString(2, "G");
        stmnt.setString(3, "mg_" + System.currentTimeMillis() + "@example.com"); // 避免EMAIL唯一约束冲突
        stmnt.setString(4, "515.123.1111");
        stmnt.setString(5, "IT_PROG");
        stmnt.setInt(6, deptId);
        
        db.execInsert(stmnt);
        stmnt.close();
    }

    public static void main(String[] args) {
        launch(args);
        // 应用退出时关闭数据库连接
        try {
            Database.getInstance().closeConnection();
        } catch (SQLException ex) {
            Logger.getLogger(InsertRollBackApp.class.getName()).log(Level.SEVERE, "关闭连接失败", ex);
        }
    }
}

关键改进说明

  1. 单例连接管理:确保整个应用只有一个数据库连接,所有操作都在同一个事务内执行;
  2. 无需提前提交:Oracle的序列主键在执行INSERT后,当前会话可以直接通过getGeneratedKeys()获取,不需要提交事务——序列的递增是会话级别的,即使事务回滚,序列值不会回退,但这是正常现象,不影响数据完整性;
  3. 事务原子性:部门和员工的插入要么全部成功提交,要么全部失败回滚,不会出现孤立的部门数据;
  4. 资源泄漏修复:手动关闭ResultSet和PreparedStatement,避免数据库资源浪费。

内容的提问来源于stack exchange,提问作者Gado

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:47:10