ประกอบธุรกรรม
รวมเอฟเฟกต์อย่างปลอดภัย
ประกอบธุรกรรม เป็นบทเรียน Scala for Backend Engineering & Functional Programming ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Scala for Backend Engineering & Functional Programming และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Scala for Backend Engineering & Functional Programming มีบทเรียนทั้งหมด 4 บทเรียน
ConnectionIO เป็น Monad
พลังที่แท้จริงของ Doobie คือ ConnectionIO เป็น monad คุณสามารถรวมคำสั่งหลายรายการเป็นโปรแกรมขนาดใหญ่หนึ่งโปรแกรมได้โดยใช้ flatMap หรือการเขียนแบบ for-comprehension
ทุกสิ่งที่จัดลำดับด้วยวิธีนี้จะทำงานบนการเชื่อมต่อ เดียวกัน ภายในธุรกรรมเดียว
import doobie.implicits._
val program: ConnectionIO[Long] =
for {
id <- insertUser("Ada")
_ <- insertProfile(id)
} yield idหนึ่งธุรกรรมต่อการเรียก transact
ไม่ว่าคุณจะประกอบคำสั่งไว้กี่รายการ ConnectionIO ทั้งหมดจะกลายเป็นธุรกรรมเดียวเมื่อเรียก .transact
หากขั้นตอนใดล้มเหลว กลยุทธ์ของ Transactor จะย้อนกลับการเปลี่ยนแปลงทั้งหมด หากสำเร็จทั้งหมด ระบบจะยืนยันธุรกรรมเพียงครั้งเดียวเมื่อสิ้นสุด
val io: IO[Long] = program.transact(xa)การเขียนหลายคำสั่งแบบเป็นอะตอมิก
เนื่องจากคำสั่งที่ประกอบกันใช้ธุรกรรมเดียวกัน คุณจึงได้คุณสมบัติอะตอมิกโดยอัตโนมัติ การโอนเงินที่หักจากบัญชีหนึ่งและเพิ่มให้อีกบัญชีหนึ่งจะสำเร็จทั้งหมดหรือไม่เกิดขึ้นเลย
จะไม่มีสถานะบางส่วนคงค้าง หากการอัปเดตครั้งที่สองส่งข้อผิดพลาด
def transfer(from: Long, to: Long, cents: Int) =
for {
_ <- sql"update acct set bal = bal - $cents where id = $from".update.run
_ <- sql"update acct set bal = bal + $cents where id = $to".update.run
} yield ()การส่งและจัดการข้อผิดพลาด
ConnectionIO มีอินสแตนซ์ MonadError คุณจึงใช้ raiseError เพื่อยกเลิกธุรกรรมและเรียกใช้การย้อนกลับได้
คุณยังใช้ handleErrorWith เพื่อกู้คืนได้ แต่โปรดทราบว่าการกู้คืนเพียงอย่างเดียวไม่ได้ยกเลิกคำสั่งก่อนหน้า เว้นแต่ธุรกรรมจะถูกย้อนกลับ
import cats.syntax.all._
import doobie.free.connection.{raiseError, pure}
def debit(id: Long, c: Int): ConnectionIO[Unit] =
sql"update acct set bal = bal - $c where id = $id".update.run.flatMap {
case 1 => pure(())
case _ => raiseError(new RuntimeException("no such account"))
}ตรรกะแบบมีเงื่อนไขในธุรกรรม
เนื่องจากมันเป็นเพียง monad การควบคุมการทำงานตามปกติของ Scala จึงใช้ได้ คุณสามารถอ่านแถว แยกเส้นทางตามค่า และเขียนข้อมูลตามนั้นได้ โดยทั้งหมดเป็นอะตอมิก
วิธีนี้ทำให้กฎทางธุรกิจและการจัดเก็บข้อมูลอยู่ในหน่วยที่สอดคล้องกันหน่วยเดียว
for {
bal <- sql"select bal from acct where id = $id".query[Int].unique
_ <- if (bal >= amt)
sql"update acct set bal = bal - $amt where id = $id".update.run
else raiseError(new RuntimeException("insufficient funds"))
} yield ()จุดบันทึกสำหรับการย้อนกลับบางส่วน
หากต้องการควบคุมละเอียดขึ้น Doobie จะเปิดให้ใช้ JDBC Savepoint ผ่านพีชคณิตการเชื่อมต่อแบบอิสระ FC คุณสามารถย้อนกลับไปยังจุดบันทึกได้โดยไม่ต้องยกเลิกธุรกรรมทั้งหมด
วิธีนี้ทำให้ใช้รูปแบบลองทำงานและทางเลือกสำรองภายในธุรกรรมภายนอกหนึ่งรายการได้
import doobie.free.{connection => FC}
val withSp =
for {
sp <- FC.setSavepoint
_ <- riskyWrite.handleErrorWith(_ => FC.rollback(sp))
} yield ()การรวมคิวรีและการเขียนข้อมูล
การอ่านและการเขียนสามารถผสมกันได้อย่างอิสระในโปรแกรมเดียว คุณอาจเลือกสถานะปัจจุบัน คำนวณการเปลี่ยนแปลงใน Scala แล้วจัดเก็บผลลัพธ์ โดยทั้งหมดทำบนการเชื่อมต่อเดียวกัน
การอ่านจะมองเห็นการเปลี่ยนแปลงที่ยังไม่ยืนยันซึ่งทำไว้ก่อนหน้าในธุรกรรมเดียวกัน
for {
v <- sql"select stock from items where id = $id".query[Int].unique
_ <- sql"update items set stock = ${v - 1} where id = $id".update.run
n <- sql"select stock from items where id = $id".query[Int].unique
} yield nการนำโปรแกรมกลับมาใช้ใหม่ด้วย traverse
เนื่องจากโปรแกรมเป็นค่า คุณจึงเรียกใช้โปรแกรมเดียวกันกับรายการด้วย traverse ของ cats ได้ โดยจัดลำดับการทำงานทั้งหมดในธุรกรรมเดียว
การแทรกข้อมูลทั้งหมดจะยืนยันพร้อมกัน หรือไม่ยืนยันเลย
import cats.syntax.all._
def saveAll(names: List[String]): ConnectionIO[List[Long]] =
names.traverse(insertUser)ระดับการแยกธุรกรรม
คุณสามารถกำหนดระดับการแยกธุรกรรมภายในโปรแกรมได้ โดยใช้พีชคณิตการเชื่อมต่อก่อนเริ่มงาน
ระดับที่เข้มงวดกว่า เช่น SERIALIZABLE จะป้องกันความผิดปกติ แต่ฐานข้อมูลอาจยกเลิกธุรกรรมที่ขัดแย้งกัน ซึ่งคุณควรลองใหม่
import doobie.free.{connection => FC}
import java.sql.Connection
val serializable =
FC.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) *> programการลองใหม่เมื่อการทำให้เป็นอนุกรมล้มเหลว
การลองใหม่ควรอยู่ในชั้นเอฟเฟกต์ ไม่ใช่ในธุรกรรม หลังจาก .transact คุณจะได้ IO และสามารถลองทำธุรกรรมทั้งหมดใหม่เมื่อเกิดข้อผิดพลาดด้านการทำให้เป็นอนุกรม
การลองใหม่แต่ละครั้งเป็นธุรกรรมใหม่ที่เป็นอิสระจากครั้งก่อน
def runWithRetry(io: IO[Unit], n: Int): IO[Unit] =
io.handleErrorWith {
case _ if n > 0 => runWithRetry(io, n - 1)
case e => IO.raiseError(e)
}ทำให้ธุรกรรมสั้น
ประกอบเฉพาะงานที่ต้องเป็นอะตอมิกไว้ใน ConnectionIO เดียว อย่าดำเนินการเรียกภายนอก เช่น HTTP หรืองาน CPU ที่ใช้เวลานาน ภายในธุรกรรม เพราะจะยึดการเชื่อมต่อและล็อกไว้
อ่านข้อมูล คำนวณภายนอกหากทำได้ แล้วจึงเขียนข้อมูลในธุรกรรมที่กระชับ
// good: gather input first, then one short transactional write
val write: ConnectionIO[Int] =
sql"update users set name = $name where id = $id".update.runตรวจสอบอย่างรวดเร็ว
พิจารณาขอบเขตของธุรกรรม
สรุปทบทวน
ConnectionIO เป็น monad ดังนั้น for-comprehension จึงประกอบคำสั่งเป็นธุรกรรมอะตอมิกหนึ่งรายการต่อ .transact ข้อผิดพลาดจะย้อนกลับการเปลี่ยนแปลงทั้งหมด และ raiseError ใช้ยกเลิกอย่างตั้งใจ
ใช้จุดบันทึกสำหรับการย้อนกลับบางส่วน กำหนดระดับการแยกธุรกรรมภายในโปรแกรม ลองใหม่เมื่อการทำให้เป็นอนุกรมล้มเหลวในชั้นเอฟเฟกต์ และทำให้ธุรกรรมสั้น
คำถามที่พบบ่อย
บทเรียน “ประกอบธุรกรรม” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ประกอบธุรกรรม” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Scala for Backend Engineering & Functional Programming ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Scala for Backend Engineering & Functional Programming มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ประกอบธุรกรรม”
รวมเอฟเฟกต์อย่างปลอดภัย คุณปฏิบัติ Scala for Backend Engineering & Functional Programming ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Scala for Backend Engineering & Functional Programming หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Scala for Backend Engineering & Functional Programming บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “ประกอบธุรกรรม” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Scala for Backend Engineering & Functional Programming นี้ได้ไหม
ได้ บทเรียน Scala for Backend Engineering & Functional Programming ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ