Thursday, March 28, 2013

การใช้ Transactions ใน Java


Disabling Auto-Commit Mode

เมื่อทำการสร้าง connection นั้น จะอยู่ในโหมด Auto Commit โดยอัตโนมัติ นั่นหมายถึงว่าแต่ละคำสั่ง SQL ที่กระทำ transaction นั้นจะ Commit(ยืนยัน) หมด(ถ้าจะให้ละเอียดคือ ในคำสั่ง SQL นั้นจะ Commit เมื่อมันเสร็จสิ้นแล้ว ไม่ใช่ในขณะทำงาน(excuted) )

ในการ disable commit คือ con.setAutoCommit(false);

หลังจากที่เราได้ทำการ disable commit ไปแล้วจะไม่มีคำสั่ง SQL Statement ไหน commit จนกว่าเราจะใช้คำสั่ง commit เอง คำสั่ง SQL ทั้งหมดจะถูก excute หลังจากเรียกคำสั่ง commit คือเพิ่มเข้าไปใน transection ปัจจุบันและ commit รวมกันเป็นกลุ่ม ดังตัวอย่าง

public void updateCoffeeSales(HashMap salesForWeek)
    throws SQLException {

    PreparedStatement updateSales = null;
    PreparedStatement updateTotal = null;

    String updateString =
        "update " + dbName + ".COFFEES " +
        "set SALES = ? where COF_NAME = ?";

    String updateStatement =
        "update " + dbName + ".COFFEES " +
        "set TOTAL = TOTAL + ? " +
        "where COF_NAME = ?";

    try {
        con.setAutoCommit(false);
        updateSales = con.prepareStatement(updateString);
        updateTotal = con.prepareStatement(updateStatement);

        for (Map.Entry e : salesForWeek.entrySet()) {
            updateSales.setInt(1, e.getValue().intValue());
            updateSales.setString(2, e.getKey());
            updateSales.executeUpdate();
            updateTotal.setInt(1, e.getValue().intValue());
            updateTotal.setString(2, e.getKey());
            updateTotal.executeUpdate();
            con.commit();
        }
    } catch (SQLException e ) {
        JDBCTutorialUtilities.printSQLException(e);
        if (con != null) {
            try {
                System.err.print("Transaction is being rolled back");
                con.rollback();
            } catch(SQLException excep) {
                JDBCTutorialUtilities.printSQLException(excep);
            }
        }
    } finally {
        if (updateSales != null) {
            updateSales.close();
        }
        if (updateTotal != null) {
            updateTotal.close();
        }
        con.setAutoCommit(true);
    }
}

ใน method นี้จะ auto-commit mode เป็น disable ซึ่งหมายถึง 2 prepared statements จะ commit รวมกัน เมื่อ commit มีการเรียก
ในคำสั่ง con.setAutoCommit(true); จะหมายถึงการกลับไปยัง สถานะ default เมื่อคำสั่งต่างๆเสร็จสิ้้น

Using Transactions to Preserve Data Integrity

ตัวอย่างของ transaction isolation level คือ TRANSACTION_READ_COMMITTED จะไม่ยอมรับการเข้าถึงข้อมูลจนกว่า transaction เสร็จสิ้น พูดอย่างอย่างคือ DBMS จะไม่ยอมรับการมี dirty reads เกิดขึ้น
Transaction Isolation Level แบ่งออกเป็น 5 ระดับ ดังนี้
Isolation LevelTransactionsDirty ReadsNon-Repeatable ReadsPhantom Reads
TRANSACTION_NONENot supportedNot applicableNot applicableNot applicable
TRANSACTION_READ_COMMITTEDSupportedPreventedAllowedAllowed
TRANSACTION_READ_UNCOMMITTEDSupportedAllowedAllowedAllowed
TRANSACTION_REPEATABLE_READSupportedPreventedPreventedAllowed
TRANSACTION_SERIALIZABLESupportedPreventedPreventedPrevented
 non-repeatable read  เกิดขึ้นเมื่อ transaction A รับค่าแถว,ลำดับต่อมา transaction B update แถว และลำดับต่อมา transaction A รับค่าที่แถวเดิมอีกครั้ง transaction A รับค่าที่แถวเดิม 2 ครั้งแต่ได้ค่าต่างไปจากเดิม
 phantom read  เกิดขึ้นเมื่อ transaction A รับค่าจะแถวโดยผ่าน condition (where),ลำดับต่อมา transaction B ก็ทำการเพิ่มและ update แถวโดยผ่าน condition เดียวกันกับ transaction A ต่อมา transaction A ก็มารับค่าตาม condition อีกครั้ง แต่ปรากฏว่าเห็นแถวเพิ่มขึ้น แถวที่เพิ่มขึ้นนี้เองเรียกว่า  phantom
โดยทั่วไปเราไม่ต้องทำอะไรเกี่ยวกับ Transaction Isolation Level สามารถใช้ค่า defalut ของ DBMS ได้เลย ค่า default ของ DBMS จะขึ้นอยู่กับ DBMS ที่ใช้ เช่น  Java DB คือ TRANSACTION_READ_COMMITTED ถ้าต้องการหาว่า DBMS ใช้ Level ไหนใช้คำสั่ง Connection method getTransactionIsolation และถ้าต้องการ set เองใช้ Connection method setTransactionIsolation 
Note JDBC driver อาจจะไม่ support transaction level ทั้งหมด ถ้าไม่ support level ที่ระบุใน setTransactionIsolation ก็สามารถสลับไปใช้ level ที่สูงกว่า ถ้า driver ไม่สามารถสลับไปที่ level ที่สูงกว่าได้จะเกิด SQLException ใช้ method DatabaseMetaData.supportsTransactionIsolationLevel ในการหาว่า driver support level ไหน

Setting and Rolling Back to Savepoints

 method Connection.setSavepoint คือ set Savepoint Object ใน transaction ปัจจุบัน Connection.rollback method คือส่ง Savepoint ไปเป็น agument
ตัวอย่างถ้าต้องการเพิ่มราคาแล้วราคานั้นเกินกว่าที่กำหนดไว้ก็จะ rollback ไปค่าเริ่มต้นใหม่อีกครั้ง
public void modifyPricesByPercentage(
    String coffeeName,
    float priceModifier,
    float maximumPrice)
    throws SQLException {
    
    con.setAutoCommit(false);

    Statement getPrice = null;
    Statement updatePrice = null;
    ResultSet rs = null;
    String query =
        "SELECT COF_NAME, PRICE FROM COFFEES " +
        "WHERE COF_NAME = '" + coffeeName + "'";

    try {
        Savepoint save1 = con.setSavepoint();
        getPrice = con.createStatement(
                       ResultSet.TYPE_SCROLL_INSENSITIVE,
                       ResultSet.CONCUR_READ_ONLY);
        updatePrice = con.createStatement();

        if (!getPrice.execute(query)) {
            System.out.println(
                "Could not find entry " +
                "for coffee named " +
                coffeeName);
        } else {
            rs = getPrice.getResultSet();
            rs.first();
            float oldPrice = rs.getFloat("PRICE");
            float newPrice = oldPrice + (oldPrice * priceModifier);
            System.out.println(
                "Old price of " + coffeeName +
                " is " + oldPrice);

            System.out.println(
                "New price of " + coffeeName +
                " is " + newPrice);

            System.out.println(
                "Performing update...");

            updatePrice.executeUpdate(
                "UPDATE COFFEES SET PRICE = " +
                newPrice +
                " WHERE COF_NAME = '" +
                coffeeName + "'");

            System.out.println(
                "\nCOFFEES table after " +
                "update:");

            CoffeesTable.viewTable(con);

            if (newPrice > maximumPrice) {
                System.out.println(
                    "\nThe new price, " +
                    newPrice +
                    ", is greater than the " +
                    "maximum price, " +
                    maximumPrice +
                    ". Rolling back the " +
                    "transaction...");

                con.rollback(save1);

                System.out.println(
                    "\nCOFFEES table " +
                    "after rollback:");

                CoffeesTable.viewTable(con);
            }
            con.commit();
        }
    } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    } finally {
        if (getPrice != null) { getPrice.close(); }
        if (updatePrice != null) {
            updatePrice.close();
        }
        con.setAutoCommit(true);
    }
}

method เริ่มต้นโดยสร้าง Savepoint ด้วยคำสั่ง Savepoint save1 = con.setSavepoint();

method จะทำการตรวจสอบค่า new price ว่ามากกว่า maximum price หรือไม่ถ้ามากกว่าก็ให้กลับไปค่าปัจจุบัน con.rollback(save1);

ดังนั้น เมื่อ method commit transaction โดยเรียก Connection.commit method มันจะไม่ commit แถวใดๆที่มีความสัมพันธ์กับ Savepoint ที่มีการ rolled back มันจะ commit แถวอื่น

Releasing Savepoints

method Connection.releaseSavepoint โดยป้อน Savepoint Object เป็น parameter และจะ remove Savepoint จาก transaction ปัจจุบัน
Savepoint จะ Release โดยอัตโนมัติ เมื่อมีการ commit หรือเมื่อ transaction rolle back
การเรียน method rollback จะทำให้ออกจาก transaction และทำการ return ค่าใดที่ทำการเปลียนแปลงก่อนหน้าให้เป็นค่าเริ่มต้น ถ้าคุณทดลอง execute statment ใน transaction จะทำให้เกิด SQLException การเรียก method จะทำให้ transaction จบ และทำการเริ่มต้น transacton ใหม่อีกครั้ง