1. การทำธุรกรรมและความทันทีของข้อมูล

การทำธุรกรรม คือหน่วยตรรกะในกระบวนการดำเนินการของระบบจัดการฐานข้อมูล ประกอบด้วยชุดของการดำเนินการ การดำเนินการเหล่านี้จะสำเร็จหรือล้มเหลวพร้อมกัน และถูกจัดการเป็นอันจำนวนไม่แยกกัน ลักษณะสำคัญของการทำธุรกรรมสามารถสรุปได้เป็นรายละเอียดต่อไปนี้:

  • Atomicity: การดำเนินการทั้งหมดในการทำธุรกรรมจะสามารถสมบูรณ์ทั้งหมดหรือล้มเหลวทั้งหมดเท่านั้น ไม่สามารถสมบูรณ์บางส่วนได้
  • Consistency: การทำธุรกรรมจะต้องทำให้ฐานข้อมูลไปจากสถานะที่สอดคล้องกันไปสู่สถานะอื่นที่สอดคล้องกัน
  • Isolation: การดำเนินการของการทำธุรกรรมจะต้องได้รับการป้องกันจากการรบกวนโดยการทำธุรกรรมอื่น ๆ และข้อมูลระหว่างการทำธุรกรรมพร้อมกันจะต้องได้รับการป้องกัน
  • Durability: เมื่อการทำธุรกรรมได้รับการยืนยันแล้ว การปรับเปลี่ยนที่ทำโดยมันจะยังคงอยู่ในฐานข้อมูล

ความทันทีของข้อมูล หมายถึงการรักษาสถานะข้อมูลที่ถูกต้องและถูกต้องในฐานข้อมูลหลังจากชุดของการดำเนินการ ในสถานการณ์ที่เกี่ยวข้องกับการเข้าถึงพร้อมกันหรือข้อผิดพลาดของระบบ ความทันทีของข้อมูลนั้นมีความสำคัญมากโดยเฉพาะ และการทำธุรกรรมจะมอบหมายที่จะรักษาความทันทีของข้อมูลโดยไม่เสี่ยงเสียหายแม้ในกรณีของข้อผิดพลาดหรือความขัดแย้ง

2. ภาพรวมของเฟรมเวิร์ก ent

ent เป็นเฟรมเวิร์กของตัวบุคคลภายในฐานข้อมูล ผ่านการสร้างรหัสในภาษาโก เฟรมเวิร์กนี้ให้ API ที่ปลอดภัยต่อชนิดสำหรับดำเนินการกับฐานข้อมูล สามารถทำให้การดำเนินการกับฐานข้อมูลเป็นเรื่องน่าเข้าใจและปลอดภัย โดยสามารถหลีกเลี่ยงปัญหาด้านความปลอดภัยอย่าง SQL injection ในเชิงการทำธุรกรรม ent เฟรมเวิร์กยังมีการสนับสนุนที่แข็งแรงต่อการดำเนินการทางธุรกรรม ทำให้นักพัฒนาสามารถดำเนินการทางธุรกรรมที่ซับซ้อนด้วยรหัสที่กระชับและอำนวยความสะดวก และรับรองว่ามีการเป็นไปตามลักษณะของการทำธุรกรรมรวมไปถึงความทันทีของข้อมูล

3. เริ่มต้นทำธุรกรรม

3.1 วิธีเริ่มต้นทำธุรกรรมใน ent

ในเฟรมเวิร์ก ent การทำธุรกรรมใหม่สามารถเริ่มต้นได้ง่ายๆ ภายในขอบเขตที่กำหนดด้วยการใช้เมธอด client.Tx และคืนค่าอ็อบเจ็กต์การทำธุรกรรม Tx ตัวอย่างรหัสดังนี้:

tx, err := client.Tx(ctx)
if err != nil {
    // จัดการกับข้อผิดพลาดเมื่อเริ่มการทำธุรกรรม
    return fmt.Errorf("เกิดข้อผิดพลาดขณะเริ่มการทำธุรกรรม: %w", err)
}
// ดำเนินการต่อไปโดยใช้ tx...

3.2 ดำเนินการภายในการทำธุรกรรม

เมื่ออ็อบเจ็กต์ Tx ถูกสร้างขึ้นอย่างสำเร็จแล้ว สามารถใช้ในการดำเนินการกับฐานข้อมูลได้ การดำเนินการสร้าง ลบ อัพเดต และคิวรี่ ที่ถูกดำเนินการบนอ็อบเจ็กต์ Tx จะกลายเป็นส่วนหนึ่งของการทำธุรกรรม ตัวอย่างต่อไปนี้แสดงการดำเนินการชุดของการดำเนินการ:

hub, err := tx.Group.
    Create().
    SetName("Github").
    Save(ctx)
if err != nil {
    // หากเกิดข้อผิดพลาด กลับการทำธุรกรรม
    return rollback(tx, fmt.Errorf("ล้มเหลวในการสร้างกลุ่ม: %w", err))
}
// การดำเนินการเพิ่มเติมสามารถเพิ่มที่นี่...
// ยืนยันการทำธุรกรรม
tx.Commit()

4. การจัดการข้อผิดพลาดและการกลับคืนในการทำธุรกรรม

4.1 ความสำคัญของการจัดการข้อผิดพลาด

เมื่อทำงานกับฐานข้อมูล ข้อผิดพลาดต่างๆ เช่นปัญหาเครือข่าย การมีข้อขัดแย้งข้อมูล หรือการละเมิดความจำกัดสามารถเกิดขึ้นได้ตลอดเวลา การจัดการข้อผิดพลาดอย่างถูกต้องนั้นสำคัญอย่างมากสำหรับการรักษาความทันทีของข้อมูล ในการทำธุรกรรม หากการดำเนินการล้มเหลว การทำธุรกรรมจำเป็นต้องถูกกลับคืนเพื่อให้แน่ใจว่าการดำเนินการบางส่วนที่เสร็จแล้วซึ่งอาจเกิดความไม่สอดคล้องของฐานข้อมูลไม่ได้รับการทิ้งไว้

4.2 วิธีการในการปฏิบัติการกลับคืน

ในเฟรมเวิร์ก ent คุณสามารถใช้เมธอด Tx.Rollback() เพื่อกลับคืนการทำธุรกรรมทั้งหมด โดยทั่วไปจะกำหนดฟังก์ชันผู้ช่วย rollback เพื่อจัดการกับการกลับคืนและข้อผิดพลาด เช่นตัวอย่างด้านล่าง:

func rollback(tx *ent.Tx, err error) error {
    if rerr := tx.Rollback(); rerr != nil {
        // หากการกลับคืนล้มเหลว คืนข้อผิดพลาดเดิมและข้อผิดพลาดในการกลับคืนร่วมกัน
        err = fmt.Errorf("%w: เกิดข้อผิดพลาดขณะกลับคืนการทำธุรกรรม: %v", err, rerr)
    }
    return err
}

ด้วยฟังก์ชัน rollback นี้ เราสามารถจัดการดั้งเดิมและการกลับคืนการทำธุรกรรมเมื่อดำเนินการใดๆ ในการทำธุรกรรมล้มเหลว นี้ ยืนยันว่าแม้ในกรณีของข้อผิดพลาด มันจะไม่มีผลกระทบทางลบ โต้ะต่อความทันทีของฐานข้อมูล ฐานข้อมูลอันถูกต้องสามารถนำกลับไปโดยไม่พบผลกระทบลบสำคัญ

5. การใช้ Transactional Client

ในการใช้งานจริง อาจมีสถานการณ์ที่เราต้องการที่จะแปลงโค้ดที่ไม่ใช่ transactional เป็นโค้ดที่เป็น transactional อย่างรวดเร็ว สำหรับกรณีเช่นนี้ เราสามารถใช้ transactional client เพื่อทำการย้ายโค้ดโดยสะดวก ด้านล่างคือตัวอย่างของวิธีการแปลงโค้ดลูกค้าที่ไม่ใช่ transactional เป็นโค้ดที่รองรับการทำธุรกรรม:

// ในตัวอย่างนี้ เราห่อหุ้มฟังก์ชัน Gen ต้นฉบับไว้ใน transaction
func WrapGen(ctx context.Context, client *ent.Client) error {
    // ก่อนอื่น สร้าง transaction
    tx, err := client.Tx(ctx)
    if err != nil {
        return err
    }
    // ดึง transactional client จาก transaction
    txClient := tx.Client()
    // ดำเนินการ Gen function โดยใช้ transactional client โดยไม่เปลี่ยนแปลงโค้ด Gen ต้นฉบับ
    if err := Gen(ctx, txClient); err != nil {
        // หากเกิดข้อผิดพลาด ย้อนกลับ transaction
        return rollback(tx, err)
    }
    // หากสำเร็จ ยืนยัน transaction
    return tx.Commit()
}

ในโค้ดข้างต้น transactional client tx.Client() ถูกใช้เพื่อให้ฟังก์ชัน Gen ต้นฉบับสามารถทำงานภายใต้การรับรองของการทำธุรกรรมได้ วิธีการนี้ช่วยให้เราสามารถแปลงโค้ดลูกค้าที่ไม่ใช่ transactional เป็นโค้ดที่รองรับการทำธุรกรรมได้อย่างสะดวกโดยมีความผลกระทบต่ำต่อตรรกะต้นฉบับ

6. แนวทางการใช้งานที่ดีสำหรับการทำธุรกรรม

6.1 การจัดการ Transaction ด้วยฟังก์ชัน Callback

เมื่อตรรกะโค้ดของเรากลายเป็นซับซ้อนและเกี่ยวข้องกับการดำเนินการฐานข้อมูลหลายรายการ การบริหารจัดการเหล่านี้ด้วยการทำธุรกรรมที่เป็นกลางกลายมีความสำคัญอย่างยิ่ง ด้านล่างคือตัวอย่างของการจัดการการทำธุรกรรมผ่านฟังก์ชัน callback:

func WithTx(ctx context.Context, client *ent.Client, fn func(tx *ent.Tx) error) error {
    tx, err := client.Tx(ctx)
    if err != nil {
        return err
    }
    // ใช้ defer และ recover เพื่อจัดการกับสถานการณ์ panic ที่เป็นไปได้
    defer func() {
        if v := recover(); v != nil {
            tx.Rollback()
            panic(v)
        }
    }()
    // เรียกใช้ฟังก์ชัน callback ที่ให้มาเพื่อดำเนินการตรรกะธุรกรรมทางธุรกิจ
    if err := fn(tx); err != nil {
        // ในกรณีที่เกิดข้อผิดพลาด ย้อนกลับ transaction
        if rerr := tx.Rollback(); rerr != nil {
            err = fmt.Errorf("%w: rolling back transaction: %v", err, rerr)
        }
        return err
    }
    // หากธุรกิจทำงานได้โดยไม่เกิดข้อผิดพลาด ยืนยัน transaction
    return tx.Commit()
}

โดยใช้ WithTx ฟังก์ชันครุ่นหน้าธุรกรรมเราสามารถให้ความมั่นใจได้ว่า ถ้าเกิดข้อผิดพลาดหรือข้อยกเว้นภายในธุรกิจ การทำธุรกรรมจะถูกดำเนินที่ได้ถูกต้อง (ทำการยืนยันหรือย้อนกลับ)

6.2 การใช้ Transaction Hooks

คล้ายกับ schema hooks และ runtime hooks เราสามารถลงทะเบียน hooks ภายใน transaction ที่กำลังเปิด (Tx) ซึ่งจะถูกกระตุ้นขึ้นเมื่อ Tx.Commit หรือ Tx.Rollback:

func Do(ctx context.Context, client *ent.Client) error {
    tx, err := client.Tx(ctx)
    if err != nil {
        return err
    }
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // ตรรกะก่อนทำการยืนยันธุรกรรม
            err := next.Commit(ctx, tx)
            // ตรรกหลังทำการยืนยันธุรกรรม
            return err
        })
    })
    tx.OnRollback(func(next ent.Rollbacker) ent.Rollbacker {
        return ent.RollbackFunc(func(ctx context.Context, tx *ent.Tx) error {
            // ตรรกก่อนย้อนกลับธุรกรรม
            err := next.Rollback(ctx, tx)
            // ตรรกหลังย้อนกลับธุรกรรม
            return err
        })
    })
    // ดำเนินการธุรกิจอื่น ๆ
    //
    // 
    //
    return err
}

โดยเพิ่ม hooks ในเวลาที่ทำการยืนยันและย้อนกลับ transaction เราสามารถจัดการตรรก์กิจเพิ่มเติม เช่น logging หรือการทำความสะอาดทรัพยากร

7. เข้าใจระดับความกําหนดของธุรกรรมที่แตกต่างกัน

ในระบบฐานข้อมูล การกําหนดระดับความกําหนดของธุรกรรมเป็นสิ่งสําคัญในการป้องกันปัญหาการแข่งขันที่เกิดขึ้น (เช่นการอ่านข้อมูลที่มีการเปลี่ยนแปลงที่ยังไม่ได้รับการยืนยัน ซึ่งอาจทําให้เกิดการอ่านข้อมูลที่ไม่สมบูรณ์ อ่านข้อมูลที่ไม่สามารถทําซ้ําได้ และการอ่านข้อมูลที่แสงร่างขึ้น) นี่คือระดับความกําหนดที่มาตรฐานบางประการ และวิธีการกําหนดระดับความกําหนดของธุรกรรมในโครงสร้าง ent:

  • READ UNCOMMITTED: ระดับต่ําสุด ที่อนุญาตให้อ่านการเปลี่ยนแปลงของข้อมูลที่ยังไม่ได้รับการยืนยัน ซึ่งอาจทําให้เกิดการอ่านข้อมูลที่ไม่สมบูรณ์ อ่านข้อมูลที่ไม่สามารถทําซ้ําได้ และการอ่านข้อมูลที่แสงร่างขึ้น
  • READ COMMITTED: อนุญาตให้อ่านและยืนยันข้อมูล ป้องกันการอ่านข้อมูลที่ไม่สมบูรณ์ แต่มีโอกาสที่จะเกิดการอ่านข้อมูลที่ไม่สามารถทําซ้ําได้ และการอ่านข้อมูลที่แสงร่างขึ้น
  • REPEATABLE READ: ป้องกันการอ่านข้อมูลเดียวกันหลายครั้งภายในธุรกรรมเดียวกัน ทําให้ได้ผลลัพธ์ที่สม่งคง ป้องกันการอ่านข้อมูลที่ไม่สามารถทําซ้ําได้ แต่อาจทําให้เกิดการอ่านข้อมูลที่แสงร่างขึ้น
  • SERIALIZABLE: ระดับความกําหนดที่เข้มงวดที่สุด พยายามป้องกันการอ่านข้อมูลที่ไม่สมบูรณ์ การอ่านข้อมูลที่ไม่สามารถทําซ้ําได้ และการอ่านข้อมูลที่แสงร่างขึ้นโดยล็อคข้อมูลที่เกี่ยวข้อง

ใน ent, หากไดรเวอร์ฐานข้อมูลรองรั้งรองรับการกําหนดระดับความกําหนดของธุรกรรม จะสามารถกําหนดได้ตามนี้:

// กําหนดระดับความกําหนดของธุรกรรมเป็น repeatable read
tx, err := client.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})

การเข้าใจระดับความกําหนดของธุรกรรมและการประยุกต์ใช้งานในฐานข้อมูลเป็นสิ่งสําคัญสําหรับการรักษาความสมบูรณ์ของข้อมูลและความมั่นคงของระบบ นักพัฒนาโปรแกรมควรเลือกระดับความกําหนดที่เหมาะสมโดยพึงพิจารณาจากความต้องการของแอปพลิเคชั่นเฉพาะ đểประสบความสําเร็จในการรักษาความปลอดภัยของข้อมูลและการปรับปรุงประสิทธิภาพของระบบ