[Android] สถาปัตยกรรมของแอนดรอยด์ ตอนที่ 4 : Room Persistence Library
Room Persistence Library คือ Library สำหรับ SQLite เพื่อทำการเรียกใช้งานเกี่ยวกับฐานข้อมูล ให้ง่ายขึ้นกว่าเดิม ขณะเดียวกันก็ใช้ความสามารถจาก SQLite ได้อย่างเต็มรูปแบบ ซึ่งแต่เดิมการเรียกใช้งาน SQLite นั้นเป็นเรื่องที่ลำบาก และมันก็อาจจะเป็นกำแพงขนาดใหญ่สำหรับนักพัฒนามือใหม่ รวมถึงเจ้าของบล็อคตอนหัดเขียนแอพใหม่ๆด้วย
มาดูการสร้างตารางโดยใช้ SQLite ดิบๆ กันหน่อย
จากตัวอย่างโค้ดจะเห็นได้ว่า แค่ต้องการสร้างตารางเพื่อเก็บข้อมูล จำนวนโค้ดก็เยอะวุ่นวายเต็มไปหมด ทั้งๆที่เป็น SQL Command ง่ายๆ และถ้าหากเราเขียนเผลอเขียนคำสั่ง SQL ผิดไปนิดเดียว เช่น การเว้นวรรค ก็จะทำให้เกิด Exception ได้
จึงเป็นที่มาของ Library ที่ชื่อว่า Room ที่ทำให้การเรียกใช้งาน SQLite สามารถเรียกใช้ได้ง่ายขึ้นมากกว่าเดิม และยังมีจุดเด่นตรงที่มีการตรวจสอบ SQL Command ทุกครั้งตอน Compile (Compile Time Validation) หมายความว่าทุกครั้งที่ทำการกด Build แอพฯ ถ้าหากมี SQL Command ผิดก็จะแจ้งเตือน ไม่ต้องไปลุ้นตอนรันแอพแล้ว เยเย้ 🙏
องค์ประกอบหลักของ Room มี 3 ส่วนได้แก่
- Entity คือ คลาสของฟิลด์ข้อมูลที่ต้องการจะเก็บลงในฐานข้อมูล ซึ่งจะอยู่ในรูปของ POJO นั้นเอง
- DAO คือ คลาสที่เอาไว้กำหนดวิธีการที่เข้าถึงฐานข้อมูลโดยใช้ SQL Command ในการผูกคำสั่งเข้ากับแต่ละ method เช่น การเพิ่ม/ลบ หรือแก้ไข ข้อมูล
- Database คือ คลาสที่เอาไว้สร้างฐานข้อมูล ซึ่งจะมีการกำหนดรายการ Entity, หมายเลขเวอร์ชันของฐานข้อมูล และรายการของ DAO
เจ้าของบล็อคจะทำการยกตัวอย่างการใช้งาน เพื่อให้เข้าใจวิธีการทำงานของ Room
ซึ่งในตัวอย่างนี้จะทำการจัดการกับข้อมูลนักเรียนโดยจะมีการ เพิ่ม/ลบ และอัพเดทข้อมูล เป็นต้น Alright! Let’s Go…
ขั้นตอนที่ 1 : ทำการเพิ่ม Dependencise ในไฟล์ build.gradle (app) ดังนี้
compile "android.arch.persistence.room:runtime:1.0.0-alpha3"//ใครใช้ Kotlin ก็เปลี่ยนจาก annotationProcessor เป็น kapt นะครับ
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha3"//สำหรับสาวก RxJava2 เจ้า Room ก็ยัง Support ด้วยนะ //(Optional นะจ้า)
compile "android.arch.persistence.room:rxjava2:1.0.0-alpha3"
ขั้นตอนที่ 2 : ทำการสร้าง Entity ที่ชื่อว่า StudentEntity เพื่อกำหนดแอตทริบิวต์ของตารางของ Student และชื่อของ table ดังนี้
ทำการ Convert จาก Diagram ที่ทำการออกแบบมาใช้งานจริง ก็จะได้ดังนี้
จากตัวอย่างโค้ดเจ้าของบล็อคใช้ภาษา Kotlin ซึ่ง data class ก็คือ POJO ใน JAVA และคลาส Entity ต้องมี Annotation ที่ชื่อว่า @Entity
ซึ่งสามารถกำหนดชื่อ ของตารางได้ ถ้าหากไม่ได้กำหนดชื่อตารางค่า Default ก็จะเป็นชื่อคลาสแทน
และจาก Diagram เจ้าของบล็อคออกแบบมาว่ามีฟิลด์ id เป็น PrimaryKey ซึ่งการกำหนด PriraryKey ให้กำหนดผ่าน Annotation ที่ชื่อว่า @PrimaryKey
จากตัวอย่างเจ้าของบล็อคได้มีการกำหนดว่า autoGenerate = true
ก็หมายความว่า กำหนดให้ id เป็นแบบ autoIncrement
และถ้าหากต้องการกำหนดชื่อ Column ให้ตรงกับ Diagram ก็ให้ทำผ่าน Annotation ที่ชื่อว่า @ColumnInfo
เพราะเวลาที่ใช้งานจริงเจ้าของบล็อคชอบใช้ตัวแปรที่เป็นแบบ Camel case มากกว่า
ซึ่งการใช้งานจริงการออกแบบฟิลด์ที่ชื่อว่า address
ให้เป็น String อาจจะไม่ค่อยเหมาะสมสักเท่าไร และถ้าหากต้องการที่จะแยกออกไปเป็นอีก Object หนึ่งก็กำหนดผ่าน Annotation ที่ชื่อว่า @Embedded
ก็จะออกมาประมาณนี้
ซึ่ง AddressModel ก็เป็นคลาสที่อยู่ในรูปแบบ POJO ธรรมดาไม่ได้มีอะไรพิเศษซึ่งในตัวย่างนี้กำหนด address เป็น String ไปก่อนแล้วกันนะครับ
ขั้นตอนที่ 3 : ทำการสร้างคลาสอินเทอร์เฟซที่ชื่อว่า StudentDao.kt เพื่อทำการเข้าถึงข้อมูล คลาสนี้ต้องมี Annotation ที่ชื่อว่า @Dao
ซึ่งในคลาสนี้จะทำการกำหนด การกระทำที่เกี่ยวข้องกับฐานข้อมูลเช่น การเพิ่ม/ลบ/อัพเดท และการ Query ของข้อมูล ซึ่งจะต้องใช้ Annotation ในการระบุการกระทำได้แก่ @Insert,
@Delete
, @Update
และ @Query
ซึ่ง Annotation ที่ชื่อว่า @Query
จะอยู่รูปแบบของ SQL Command ดังนี้
และในกรณีที่เผลอตัวเขียนคำสั่ง SQL ผิด เมื่อกด Build ก็จะแจ้งเตือนดังนี้
และความคูลของ Room ก็ยังไม่หมดเพียงเท่านี้ เพราะมันยังสามารถใช้คู่กับ LiveData ได้อีกด้วย
@Query("SELECT * FROM student")
fun getStudentAll(): LiveData<List<StudentEntity>>
นั่นก็หมายความว่าเมื่อใช้คู่กับ LiveData
ก็สามารถนำใช้ใน ViewModel
ได้สบาย
ยัง ยังไม่หมดเพียงแค่นี้ เพราะยังสามารถใช้คู่กับ RxJava2 ได้อีกด้วย (จัดเต็มมากสำหรับ Room)
@Query("SELECT * FROM student")
fun getStudentAll(): Flowable<List<StudentEntity>>
ขั้นตอนที่ 4 : ทำการสร้างคลาสที่ถือครอง Database ที่ชื่อว่า AppDatabase.kt โดยให้ทำการสืบทอดมาจากคลาส RoomDatabase() ซึ่งในคลาสนี้จะกำหนดรายการของ Entity
และหมายเลขเวอร์ชันของฐานข้อมูล โดยต้องมี Annotation ที่ชื่อว่า @Database
เป็นตัวระบุไว้ด้านบน ดังนี้
โดยปกติการเรียกใช้งาน Database จากคลาส AppDatabase.kt ที่เจ้าของบล็อคได้สร้างขึ้นจะเรียกใช้งานก็จะเป็นในลักษณะนี้
val db = Room.databaseBuilder(context, AppDatabase.class, "db-name").build();
ซึ่งถ้าหากประกาศแบบนี้ทุกครั้งที่ต้องการใช้ก็ดูเหมือนจะเหนื่อยเกินไปเจ้าของบล็อคจึงได้ทำการสร้าง static method ที่ชื่อ getAppDatabase(…) เอาไว้เรียกใช้งาน Database เพื่อให้เรียกใช้ที่เดียว
แค่นี้เราก็ได้ทำการตั้งค่าการใช้งาน Room Persitance เรียบร้อยแล้วต่อไปก็มาถึงในส่วนของการเรียกใช้งาน ซึ่งในส่วนของการเรียกใช้งานก็สามารถเรียกใช้งานง่ายๆ ได้ดังนี้
และเมื่อกด Run กด ก็จะได้ผลดังนี้…
บู้ม 💥💥 กลายเป็นโกโก้ครั้นช์ เพราะว่าการเรียกใช้งาน
Room
มีข้อแม้อยู่ว่าต้องไปทำงานที่Background Thread
เท่านั้น ไม่สามารถเรียกใช้งานบนMain Thread
ได้เพื่อป้องกันไม่ให้เกิดUI Blocking
กับตัวแอพ เพราะการทำงานอาจจะต้องใช้เวลานาน เพราะฉะนั้นแล้วก็ต้องทำการอันเชิญ AsyncTask เข้ามาช่วยแต่เจ้าของบล็อคชอบ RxJava มากกว่า เจ้าของบล็อคจึงทำการแก้ไขโดยใช้ RxJava ก็จะได้ดังนี้
เพียงเท่านี้ก็สามารถเรียกใช้เจ้า Room ได้อย่างถูกวิธีแล้ว ซึ่งการอัพเดท, ลบ หรือการดึงข้อมูลก็จะอยู่ในลักษณะเดียวกัน แต่จะแตกต่างตรงที่ถ้าการดึงข้อมูลใช้คู่กับ LiveData หรือ RxJava2 ลักษณะการเรียกใช้งานก็จะประมาณนี้
กรณีที่ 1 : ใช้งานคู่กับ LiveData
appDatabase.studentDao().getStudentAll()
.observe(this, Observer {
it?.forEach { Log.d(TAG, it.firstName) }
})
กรณีที่ 2 : ใช้งานคู่กับ RxJava2
appDatabase.studentDao().getStudentAll()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { it.forEach { Log.d(TAG, it.firstName) } }
และจาก Databse Diagram ของ Student ที่เจ้าของบล็อคได้ทำการออกแบบไว้ เมื่อมีการเก็บข้อมูลไปได้สักระยะหนึ่งแล้ว มีฟีเจอร์ใหม่ที่ต้องทำการเก็บข้อมูลอายุ ของนักเรียนโดยมีข้อแม้ว่าข้อมูลที่ถูกเก็บไว้ก่อนหน้านี้ต้องไม่หาย ซึ่งลักษณะนี้จะเรียกว่าการ Migration
ของ Database ซึ่งการทำ Database migration ของ Room ก็ทำได้ในลักษณะนี้
- ทำการเพิ่มฟิลด์อายุใน StudentEntity.kt ดังนี้
2. ทำการ Migration ของ Database จากเวอร์ชัน 1 ไป เวอร์ชัน 2 ในคลาส AppDatabase.kt โดยเจ้าของบล็อคได้ทำการเพิ่ม Column ที่ชื่อว่า age จากเดิมที่เวอร์ชัน 1 ไม่มี ดังนี้
เพียงเท่านี้ข้อมูลเดิมก็จะไม่หายไป และฟิลด์อายุของข้อมูลเดิมก็จะมีค่า null
แทน
เพิ่มเติม
ถ้าต้องการอยากได้ไฟล์ schemas ของ Database ในแต่ละเวอร์ชัน ซึ่งไฟล์จะมีรูปแบบเป็นไฟล์ JSON ก็มาทำการตั้งค่าที่ไฟล์ build.gradle (app) ดังนี้
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}
และเมื่อกด Run แอพ เราก็จะได้ไฟล์ JSON ซึ่งเป็น schemas ของ Database ในแต่เวอร์ชัน ดังนี้
สรุป
การมาของ Library ที่ชื่อว่า Room Persistence ทำให้นักพัฒนาแอพแอนดรอย์หมดปัญหาในเรื่องของการเรียกใช้งาน SQLite ไปสักที เพราะเดิมทีต้องนั่งเขียนโค้ดการเรียกใช้วุ่นวายพอสมควร แถมยังไม่มีการตรวจสอบคำสั่ง SQL ให้อีกในตอน Compile ก็ต้องไปรอลุ้นกันเอาเองตอน Run แอพ และยังช่วยลดจำนวนโค้ดที่ต้องทำการ Convert ข้อมูลให้อยู่ในรูปของ Object ซึ่งตัวของ Room ก็จัดการเรื่องพวกนี้ให้เราหมดแล้ว และใครใช้ Retrofit อยู่ก็คงสักเกตได้ว่าโครงสร้างของ Room มันใกล้เคียงกันมาก แถมยังมีการเรียกใช้งานที่ยืดหยุ่น ไม่ว่าจะเป็นการเรียกใช้งานผ่าน Live Data หรือ RxJava สุดท้ายตัวอย่างในบทความนี้เป็นเพียงความสามารถส่วนหนึ่งของ Room เท่านั้น
แหล่งอ้างอิง (ตั้งแต่ตอนที่ 1 ถึง 4)