Spring Jpa connect multiple database at runtime
สำหรับตอนนี้เราจะมาทดลองทำอะไรแปลกๆที่ไม่ค่อยมี Usecase ใช้งานจริงมากเท่าไหร่ แต่ก็เป็น Case ที่น่าสนใจที่พอลองทำก็สนุกดี เลยลองเอามาเขียนตอนนี้เผื่อว่าจะมีคนเจอ Case ที่ต้องใช้จะได้ไปใช้บ้าง ซึ่งกรณีที่ว่าคือการต่อหลาย Database ในขณะ Runtime นั่นเอง
มาดูว่าเราจะทำอะไรกัน
จากภาพเราจะทำการต่อ Database 2 จำพวกหลักๆ โดยพวกแรกผมเรียกว่าพวก DatabaseInfo shcema ตัว จำพวกนี้จะมี database แค่ลูกเดียว อันนี้ไม่มีปัญหาอะไร ปัญหาอยู่ที่จำพวกที่ 2 ซึ่งก็คือ Account อันนี้ผมสร้างสถานการณ์ว่าเราแยก Account ของสาขา Thailand และ สาขา England เป็นคนละ Database แต่ทั้งสอง Database มี Schema เหมือนกัน ตอนนี้อาจจะมีคำถามแล้วว่า ทำไมต้องทำแบบนี้ คือในกรณีจริงๆอาจจะมีก็ได้คือ เราทำการแยก database ตามลูกค้าแต่ล่ะเจ้าออกจากกันเพื่อให้ข้อมูลแยกจากกันโดยสมบูรณ์ ประมาณนั้น คราวนี้มีประเด็นคือเราอยากเขียน App บางตัวไปจัดากร Database เหล่านั้น เช่น Summary data หรือไป Get ข้อมูล และด้วยอยากแยกมันจาก core หลัก App ว่าง่ายๆเราไม่อยากแตะ Code เก่า เราอยากเริ่มใหม่จะได้ใช้เทคโนโลยีที่ทันสมัย แต่จริงๆวิธีนี้ก็ไม่ค่อยถูกต้องเพราะคุณไปจิ้ม Database คนอื่นตรงๆ แต่วิธีนี้ก็ง่ายสุดในการ Maintainace แล้ว
มาดู Code กัน
สำหรับใครอยากดู Code เต็มไปดูได้ที่ https://github.com/surapong-taotiamton/multiple-db-at-runtime ตรงส่วน application.properties ที่เป็นรายละเอียดเกี่ยวกับค่า database ก็เปลี่ยนให้ถูกต้องตามที่ต้องการ
sql สำหรับสร้าง Table : DatabaseInfo สำหรับ database : center
1 |
|
sql สำหรับสร้าง Table : account สำหรับ database : branch ต่างๆ
1 | CREATE TABLE `account` ( |
CenterDataSourceConfig
Code อาจจะยาวแต่จะค่อยอธิบายไปทีล่ะส่วน
1 |
|
1 |
ส่วนนี้เป็นการบอกว่าเป็นส่วน Configuration เวลา Spring scan ไฟล์นี้จะได้ทราบว่าต้องมาทำการสร้าง Bean
1 |
ส่วนนี้เป็นการบอกว่าให้ Spring ทำการสร้าง JpaRepository นั้นตั้งค่าอะไรบ้าง
- basePackages : ส่วนนี้บอกว่าตัว Repository จาก package ไหน
- entityManagerFactoryRef : อันนี้เป็นการบอกว่าตัว entityManagetFactory ที่จะใช้เนี่ยจะใช้จาก Bean ที่ชื่อว่าอะไร
- transactionManagerRef : อันนี้บอกว่าตัว transactionManager ใช้จาก Bean ที่ชื่อว่าอะไร
1 |
|
ส่วนนี้จะเป็นการสร้างตัว DataSourceProperties ว่าง่ายๆคือบอกว่า datasource นี้มันไปต่อที่ไหน username password อะไร ใช้ driver ตัวไหน มีกี่ connection pool อะไรประมาณนั้น ซึ่งอ่านตรงนี้ก็อาจจะงงว่าค่ามันมาจากไหน อันนี้ค่ามันมาจากไฟล์ application.properites ครับ โดยตรงตัว @ConfigurationProperties(prefix = “spring.center.datasource”) ตรงนี้เป็นการบอกว่าไปเอาค่าจากตัว key ที่ขึ้นต้นด้วย spring.center.datasource ครับ โดยสามารถไปอ่านรายละเอียดการทำงานของ @ConfigurationProperties ได้ที่ https://www.baeldung.com/configuration-properties-in-spring-boot
1 |
|
ส่วนนี้คือการสร้าง dataSource ครับ โดยจะเห็นว่ามีการตัว DataSourceProperties ที่สร้างจากด้านบนมาสร้างโดยส่วนนี้เราจะใช้ตัว HikariDataSource เป็น DataSource นะครับ
1 |
|
ส่วนนี้เป็นการสร้างตัว EntityManagerFactory จะเห็นว่ามีการเอา dataSource มาใช้งาน ส่วนต่อไปที่อยากให้ลองดูคือส่วนที่นี้
1 | em.setPackagesToScan(new String[] { "blog.surapong.example.dynamicdb.entity.center" }); |
ตรงส่วนนี้เป็นการบอกว่า Entity ที่เราจะใช้นั้นอยู่ที่ package ไหนครับ
1 | properties.put("hibernate.dialect", hibernateDialect); |
ส่วนตรงนี้จะเป็นการบอกว่า Jpa ที่จะเนี่ยจะใช้ dialect ตัวไหน (ง่ายคือจะใช้ syntax ของ sql แบบไหน ซึ่งถ้าใช้ mysql ก็ต้องไป set ที่เป็นแบบ mysql ) ซึ่งในตัวอย่างผมจะใช้เป็น mariadb ซึ่งตาม manual บอกให้ใช้ตัว org.hibernate.dialect.MySQL5InnoDBDialect ครับ
1 |
|
ส่วนก็คือการสร้าง TransactionManager ครับ
จากทั้งหมดที่บอกมาเราก็จะสามารถสร้างตัว Jpa ที่ติดต่อกับ Database ได้แล้ว ซึ่งตัว Center นั้นต่อแค่ database ตัวเดียวตัว DataSource มันเลยมีแค่ค่าเดียว แต่ถ้าต้องต่อหลาย Database ก็กลายเป็นต้องมีหลาย Datasoruce แต่ที่เราเขียนมันรับได้แค่ DataSource เดียว แล้วเราจะทำยังไงล่ะ
AbstractRoutingDataSource
ปัญหานี้ถูกแก้ได้โดยง่ายเพราะใช้หลักการ OOP หากเราลองไปไล่ดู Code ตรง DataSource จะพบว่ามันเป็นแค่ Interface นั่นแปลว่าถ้าเรา Implement ตัว DataSource ที่ทำงานแบบต่อหลาย Database ขึ้นมาภายใต้ Interface DataSource เดิม เราก็สามารถส่ง DataSource ที่เรา Implement ไปแทนก็จะสามารถทำงานได้ ซึ่งเหล่านักพัฒนาก็ได้ลงเห็นตรงส่วนนี้ก็ได้สร้างตัว AbstractRoutingDataSource ที่ทำงานให้ต่อหลาย Database ไว้เป็นฐานให้เราแล้ว เหลือแค่เราไป Extend แล้วเพิ่ม method ที่เกี่ยวกับ Logic ในการเลือกว่าจะใช้ DataSource ไหนมาทำงานลงไปเอง
DataSourceRouting (Implement เอง)
ตัว Class มันยาวมากแต่จุดที่สำคัญมีแค่ไม่กี่จุดครับ
1 |
|
1 |
|
นี่คือ Method ที่เราต้องทำการ Override ซึ่ง method นี้จะเป็น Logic ที่เราต้องบอกกับว่าเราจะใช้ datasource ตัวไหนในการทำงาน ซึ่งตัว AbstractRoutingDataSource เนี่ยเขาจะมี attribute ที่ชื่อ mapDataSource ไว้เก็บค่า key กับ DataSource โดย method ให้เรา return key ที่ Map กับ datasource ที่เราต้องการคืนไป ซึ่งจะเห็นว่าเขาโคตรเปิดกว้างให้เรา Implement อะไรก็ได้ตามใจเราเลย ซึ่ง Code ของผมไม่ได้ทำอะไรมากไปกว่าการไปเอาค่าจาก BranchDataSourceContext ที่เก็บค่าไว้ว่าอยากจะใช้ datasource ไหน ลองอ่าน Logic เอาเลยครับ ที่มันดูยาวๆเพราะทำแบบ ถ้าไม่มี DataSource ก็ไปหาข้อมูลใน DatabaseInfo ว่ามีไหม ถ้ามีก็เอามาสร้าง dataSource แล้วก็เพิ่มเข้าไปใน map เอง
1 | public DataSource createDataSource(String databaseInfoId) { |
ส่วนนี้คือการสร้าง DatatSource จะเห็นว่าต่างจากตอนสร้างใน Center แต่ไม่ต่างอะไรมากแค่ทำการสร้างแบบ Manual เท่านั้นเอง ส่วนข้อมูลพวก username,password, host, dbName มาจาก DatabaseInfo ใน database
BranchDataSourceContext
1 | public class BranchDataSourceContext { |
คราวนี้อาจจะสงสัยเฮ้ยคุณเลือกว่าใช้ DataSource นี้ DataSouce ยังไง รู้ได้ไงว่าต้องใช้ตัวนี้ตัวนั้น คืออันนี้ผมเลือกว่าจะใช้ DataSouce ไหนตาม ค่าที่ถูก Set ค่าไว้ ว่าง่ายๆผมจะมีตัวที่เรียกใช้ตัว BranchDataSourceContext แล้ว set ค่าว่าจะใช้ตัวไหน ซึ่งคราวนี้ก็จะมีประเด็นว่า ถ้ามันทำงานหลายๆ Thread พร้อมกันจะเกิดอะไรขึ้น ซึ่งผมก็แก้ปัญหาโดยการใช้ ThreadLocal ของ Java มาเก็บค่าให้กับ Thread นั้นว่ากำลังใช้ Thread อะไรอยู่นั่นเอง
BranchDataSourceConfig
1 |
|
อันนี้จะเห็นว่าแทบไม่ต่าง CenterDataSource เลย แค่ต่างกันตรง Config ค่าแล้วก็ตรง DataSource เท่านั้นเอง
AccountController
1 |
|
ส่วนนี้เป็นส่วนการจำลองการทำงานหลาย Database ดูเหมือนเยอะแต่ไม่มีอะไรมากลองมาอ่านทีละส่วนกันครับ
1 |
|
ส่วนนี้จะเป็นการสร้างขารับ Http request โดยรับ query parameter ที่ชื่อ db เข้ามาโดยค่า db นั้นจะเป็นตัวบอกว่าใช้ Database อันไหน ซึ่งจะเห็นว่ามีการใช้ BranchDataSourceContext.setCurrentBranchDataSourceId(dbInfoId); อันนี้เป็นการ Set แล้วว่าจะใช้ database ตัวไหนผ่าน BranchDataSourceContext ซึ่งตัว DataSourceRouting ของเราจะใช้ BranchDataSourceContext ตัดสินใจต่อใน method : determineCurrentLookupKey ซึ่งเรามาลองทดลองดูว่าจะได้ผลลัพธ์อะไร
ทดลอง /test/insert
เมื่อทดลองยิงไปที่ /test/insert โดยใช้ query param เป็น england จะได้ผลลัพธ์ดังภาพ ขอรวมผลลัพธ์จากการเรียกผ่าน browser และผลลัพธ์จากการ Query Database ซึ่งจะเห็นว่ามีข้อมูล insert เข้าไปใน Db จริงๆ และค่า accountName นั้นมีค่าตรงกันกับผลลัพธ์จาก Browswer
คราวนี้มาทดลองโดยเปลี่ยน query param เป็น thailand กันดูบ้างซึ่งผลลัพธ์เป็นดังภาพ
ซึ่งจากการทดลองก็พบว่าทำงานได้ถูกต้อง
ทดลอง Transactional กัน
ก็ตามสูตรครับถ้าใช้ Relational Database Feature ที่เราคาดหวังอยากได้จากมันคือ Transaction ดังนั้นเราก็ต้องมา Test ว่ามันทำงานได้จริงรึเปล่าโดยเราทดสอบผ่าน ตัว AccountController ที่ไปเรียกใช้งานตัว AccountService ซึ่งมีการเขียนจงใจให้ Error เพื่อทดสอบว่าข้อมูลที่ Insert ไปต้องหายไปเสมือนว่าไม่เคยเกิดขึ้น
1 |
|
สังเกตตรง @Transactional(transactionManager = “branchTransactionManager”) นั้นต้องมีการกำหนดชื่อด้วย เพราะตัว transactionManager นั้นถูกสร้างขึ้นมาสองตัวนั่นคือของ center กับ ของ branch เราจึงต้องกำหนดชื่อมันเลยว่าใช้ตัวไหน ซึ่งจากการทดลองพบว่าไม่มีข้อมูลโผล่ไปเพิ่มใน Database ของ england มีแค่ 1 ROW เท่าเดิม นั่นก็แปลว่า Feature : Transaction ของเราทำงาน
สรุป
สำหรับตอนนี้เราก็ได้ทดลองทำอะไรแปลกๆก็คือทดลองต่อหลาย Database ในเวลาเดียวกันแบบ Runtime กันแล้ว หวังว่าคงจะสนุกและได้แนวทางไปประยุกต์ใช้ในงานต่างๆกันนะครับ
Ref
- https://docs.jboss.org/hibernate/orm/5.1/userguide/html_single/Hibernate_User_Guide.html#configurations-hbmddl
- https://github.com/bcssp10/multi-tenant
- https://www.websparrow.org/spring/spring-boot-dynamic-datasource-routing-using-abstractroutingdatasource
เพลงประกอบการเขียน Blog
อยู่ Joox ก็แนะนำเพลงนี้มาให้ฟังซึ่งงงมากว่าเฮ้ยไม่ได้อยู่ใน Playlist อะไรเลย เด้งมาได้ไง พอฟังๆไปก็รู้สึกคุ้นมากๆเลยไป Search หาใน Youtube ก็เลยร้องอ๋อเลยเพราะเป็นเพลง OST ของละครช่อง 5 สมัยตอนเด็ก