【Android-Jetpack进阶】5、Room 数据库、ViewModel 内通过 Database 查到 LiveData 并通知变化、用 createFromAsset() 预填充数据库

五、Room 操作数据库

5.1 用 Entity、Dao、Database 操作数据库

数据库的每个表,都对应一个 Entity,一个 Dao(Dao 负责增删改查操作)

新建项目 Jetpack5RoomTest,在 build.gradle 添加如下依赖:

plugins {
    
    
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}

dependencies {
    
    
    def room_version = "2.4.3"
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
}

首先,新建 database/Student.kt,代码如下:

package com.bignerdranch.android.jetpack5roomtest.database

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey


@Entity(tableName = "student")
class Student {
    
    
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
    var id = 0

    @ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
    var name: String

    @ColumnInfo(name = "age", typeAffinity = ColumnInfo.TEXT)
    var age: String

    /**
     * Room会使用这个构造器来存储数据,也就是当你从表中得到Student对象时候,Room会使用这个构造器
     */
    constructor(id: Int, name: String, age: String) {
    
    
        this.id = id
        this.name = name
        this.age = age
    }

    /**
     * 由于Room只能识别和使用一个构造器,如果希望定义多个构造器,你可以使用Ignore标签,让Room忽略这个构造器
     * 同样,@Ignore标签还可用于字段,使用@Ignore标签标记过的字段,Room不会持久化该字段的数据
     */
    @Ignore
    constructor(name: String, age: String) {
    
    
        this.name = name
        this.age = age
    }
}

然后,新建 database/StudentDao.kt,封装增删改查操作,代码如下:

package com.bignerdranch.android.jetpack5roomtest.database

import androidx.room.*

@Dao
interface StudentDao {
    
    
    @Insert
    fun insertStudent(student: Student?)

    @Delete
    fun deleteStudent(student: Student?)

    @Update
    fun updateStudent(student: Student?)

    @get:Query("SELECT * FROM student")
    val studentList: List<Student?>?

    @Query("SELECT * FROM student WHERE id = :id")
    fun getStudentById(id: Int): Student?
}

然后,新建 database/MyDatabase.kt,其中有 studentDao 属性,代码如下:

package com.bignerdranch.android.jetpack5roomtest.database

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [Student::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
    
    
    abstract fun studentDao(): StudentDao?

    companion object {
    
    
        private const val DATABASE_NAME = "my_db"
        private var databaseInstance: MyDatabase? = null

        @Synchronized
        fun getInstance(context: Context): MyDatabase? {
    
    
            if (databaseInstance == null) {
    
    
                databaseInstance = Room.databaseBuilder(context.applicationContext, MyDatabase::class.java, DATABASE_NAME).build()
            }
            return databaseInstance
        }
    }
}

activity_main.xml 的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btnInsertStudent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="12dp"
        android:layout_gravity="center_horizontal"
        android:textAllCaps="false"
        android:text="Add a Student"/>

    <ListView
        android:id="@+id/lvStudent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

activity_main.xml 的布局如下:

在这里插入图片描述

然后,新建 list_item_student.xml 布局,布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
    android:paddingTop="12dp"
    android:paddingBottom="12dp">

    <TextView
        android:id="@+id/tvId"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_weight="1"/>

    <TextView
        android:id="@+id/tvName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_weight="1"/>

    <TextView
        android:id="@+id/tvAge"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_weight="1"/>

</LinearLayout>

list_item_student.xml 的布局如下:

在这里插入图片描述

新建 dialog_layout_student.xml,布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <EditText
        android:id="@+id/etName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Name"
        android:layout_weight="1"
        android:autofillHints="" />

    <EditText
        android:id="@+id/etAge"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Age"
        android:layout_weight="1"
        android:autofillHints="" />

</LinearLayout>

dialog_layout_student.xml 的效果如下:

在这里插入图片描述

接下来,在 MainActivity 中可通过 database 来操作数据库,代码如下:

package com.bignerdranch.android.jetpack5roomtest

import android.content.DialogInterface
import android.os.AsyncTask
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.widget.AdapterView.OnItemLongClickListener
import android.widget.EditText
import android.widget.ListView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.bignerdranch.android.jetpack5roomtest.database.MyDatabase
import com.bignerdranch.android.jetpack5roomtest.database.Student


class MainActivity : AppCompatActivity() {
    
    
    private var myDatabase: MyDatabase? = null
    private var studentList: MutableList<Student?>? = ArrayList()
    private var studentAdapter: StudentAdapter? = null
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<View>(R.id.btnInsertStudent).setOnClickListener {
    
     openAddStudentDialog() }
        val lvStudent: ListView = findViewById(R.id.lvStudent)
        studentAdapter = StudentAdapter(this@MainActivity, studentList)
        lvStudent.adapter = studentAdapter
        lvStudent.onItemLongClickListener = OnItemLongClickListener {
    
     _, _, position, _ ->
            updateOrDeleteDialog((studentList as ArrayList<Student?>)[position])
            false
        }
        myDatabase = MyDatabase.getInstance(this)
        QueryStudentTask().execute()
    }

    private fun updateOrDeleteDialog(student: Student?) {
    
    
        val options = arrayOf("更新", "删除")
        AlertDialog.Builder(this@MainActivity)
            .setTitle("")
            .setItems(options, DialogInterface.OnClickListener {
    
     _, which ->
                if (which == 0) {
    
    
                    openUpdateStudentDialog(student)
                } else if (which == 1) {
    
    
                    if (student != null) {
    
    
                        DeleteStudentTask(student).execute()
                    }
                }
            }).show()
    }

    private fun openAddStudentDialog() {
    
    
        val customView: View = this.layoutInflater.inflate(R.layout.dialog_layout_student, null)
        val etName: EditText = customView.findViewById(R.id.etName)
        val etAge: EditText = customView.findViewById(R.id.etAge)
        val dialog: AlertDialog = AlertDialog.Builder(this@MainActivity).create()
        dialog.setTitle("Add Student")
        dialog.setButton(DialogInterface.BUTTON_POSITIVE, "OK", DialogInterface.OnClickListener {
    
     _, _ ->
            if (TextUtils.isEmpty(etName.text.toString()) || TextUtils.isEmpty(etAge.text.toString())) {
    
    
                Toast.makeText(this@MainActivity, "输入不能为空", Toast.LENGTH_SHORT).show()
            } else {
    
    
                InsertStudentTask(etName.text.toString(), etAge.text.toString()).execute()
            }
        })
        dialog.setButton(DialogInterface.BUTTON_NEGATIVE, "CANCEL", DialogInterface.OnClickListener {
    
     _, _ -> dialog.dismiss() })
        dialog.setView(customView)
        dialog.show()
    }

    private fun openUpdateStudentDialog(student: Student?) {
    
    
        if (student == null) {
    
    
            return
        }
        val customView: View = this.layoutInflater.inflate(R.layout.dialog_layout_student, null)
        val etName: EditText = customView.findViewById(R.id.etName)
        val etAge: EditText = customView.findViewById(R.id.etAge)
        etName.setText(student.name)
        etAge.setText(student.age)
        val dialog: AlertDialog = AlertDialog.Builder(this@MainActivity).create()
        dialog.setTitle("Update Student")
        dialog.setButton(DialogInterface.BUTTON_POSITIVE, "OK", DialogInterface.OnClickListener {
    
     _, _ ->
            if (TextUtils.isEmpty(etName.text.toString()) || TextUtils.isEmpty(etAge.text.toString())) {
    
    
                Toast.makeText(this@MainActivity, "输入不能为空", Toast.LENGTH_SHORT).show()
            } else {
    
    
                UpdateStudentTask(student.id, etName.text.toString(), etAge.text.toString()).execute()
            }
        })
        dialog.setButton(DialogInterface.BUTTON_NEGATIVE, "CANCEL", DialogInterface.OnClickListener {
    
     dialog, _ -> dialog.dismiss() })
        dialog.setView(customView)
        dialog.show()
    }

    private inner class InsertStudentTask(var name: String, var age: String) : AsyncTask<Void?, Void?, Void?>() {
    
    
        override fun doInBackground(vararg params: Void?): Void? {
    
    
            myDatabase!!.studentDao()!!.insertStudent(Student(name, age))
            studentList!!.clear()
            studentList!!.addAll(myDatabase!!.studentDao()!!.studentList!!)
            return null
        }

        override fun onPostExecute(result: Void?) {
    
    
            super.onPostExecute(result)
            studentAdapter?.notifyDataSetChanged()
        }
    }

    private inner class UpdateStudentTask(var id: Int, var name: String, var age: String) : AsyncTask<Void?, Void?, Void?>() {
    
    
        override fun onPostExecute(result: Void?) {
    
    
            super.onPostExecute(result)
            studentAdapter?.notifyDataSetChanged()
        }

        override fun doInBackground(vararg params: Void?): Void? {
    
    
            myDatabase!!.studentDao()!!.updateStudent(Student(id, name, age))
            studentList!!.clear()
            studentList!!.addAll(myDatabase!!.studentDao()!!.studentList!!)
            return null
        }
    }

    private inner class DeleteStudentTask(var student: Student) : AsyncTask<Void?, Void?, Void?>() {
    
    
        override fun onPostExecute(result: Void?) {
    
    
            super.onPostExecute(result)
            studentAdapter?.notifyDataSetChanged()
        }

        override fun doInBackground(vararg params: Void?): Void? {
    
    
            myDatabase!!.studentDao()!!.deleteStudent(student)
            studentList!!.clear()
            studentList!!.addAll(myDatabase!!.studentDao()!!.studentList!!)
            return null
        }
    }

    private inner class QueryStudentTask : AsyncTask<Void?, Void?, Void?>() {
    
    
        override fun onPostExecute(result: Void?) {
    
    
            super.onPostExecute(result)
            studentAdapter?.notifyDataSetChanged()
        }

        override fun doInBackground(vararg params: Void?): Void? {
    
    
            studentList!!.clear()
            studentList!!.addAll(myDatabase!!.studentDao()!!.studentList!!)
            return null
        }
    }
}

新建 StudentAdapter.kt,绑定 activity_main.xml 的 ListView,代码如下:

package com.bignerdranch.android.jetpack5roomtest

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
import com.bignerdranch.android.jetpack5roomtest.database.Student


class StudentAdapter(context: Context, private val data: MutableList<Student?>?) : BaseAdapter() {
    
    
    private val layoutInflater: LayoutInflater

    internal inner class ViewHolder {
    
    
        var tvId: TextView? = null
        var tvName: TextView? = null
        var tvAge: TextView? = null
    }

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? {
    
    
        var convertView: View? = convertView
        val viewHolder: ViewHolder
        if (convertView == null) {
    
    
            convertView = layoutInflater.inflate(R.layout.list_item_student, null)
            viewHolder = ViewHolder()
            viewHolder.tvId = convertView.findViewById(R.id.tvId)
            viewHolder.tvName = convertView.findViewById(R.id.tvName)
            viewHolder.tvAge = convertView.findViewById(R.id.tvAge)
            convertView.tag = viewHolder
        } else {
    
    
            viewHolder = convertView.tag as ViewHolder
        }
        viewHolder.tvId!!.text = (data!![position]?.id).toString()
        viewHolder.tvName!!.text = data[position]!!.name
        viewHolder.tvAge!!.text = data[position]!!.age
        return convertView
    }

    override fun getCount(): Int {
    
    
        return data?.size ?: 0
    }

    override fun getItem(position: Int): Student? {
    
    
        return data?.get(position)
    }

    override fun getItemId(position: Int): Long {
    
    
        return position.toLong()
    }

    init {
    
    
        layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
    }
}

运行后,可以通过 Room 对数据库做添加、更新、删除、查询,效果如下:

在这里插入图片描述

项目github代码详见

5.2 ViewModel 内:通过 Room.Database 查到 LiveData 数据,在外部监听 LiveData

当 Room 变化时,通过 ViewModel 内的 LiveData 通知页面数据的变化,架构如下:

在这里插入图片描述

有两种实例化数据库对象的方式:

  • 在 Application 中实例化数据库,ViewModel 调用它
  • 用 AndroidViewModel 的构造函数参数的 Application 参数(作为 Context 的子类),实例化数据库

新建项目(拷贝上文5.1节的项目),项目github详见

首先,修改 StudentDao.kt,用 LiveData 将 List 包装起来,代码如下:

package com.bignerdranch.android.jetpack5roomwithlivedataandviewmodeltest.database

import androidx.lifecycle.LiveData
import androidx.room.*

@Dao
interface StudentDao {
    
    
    @Insert
    fun insertStudent(student: Student?)

    @Delete
    fun deleteStudent(student: Student?)

    @Update
    fun updateStudent(student: Student?)

    @Query("SELECT * FROM student")
    fun getStudentList(): LiveData<List<Student?>?>? //希望监听学生表的变化,为其加上LiveData

    @Query("SELECT * FROM student WHERE id = :id")
    fun getStudentById(id: Int): Student?
}

其次,新建 StudentViewModel 类,该类继承自 AndroidViewModel,其中有 Database 和 LiveData,代码如下:

package com.bignerdranch.android.jetpack5roomwithlivedataandviewmodeltest

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import com.bignerdranch.android.jetpack5roomwithlivedataandviewmodeltest.database.MyDatabase
import com.bignerdranch.android.jetpack5roomwithlivedataandviewmodeltest.database.Student


class StudentViewModel(application: Application) : AndroidViewModel(application) {
    
    
    private val myDatabase: MyDatabase?
    val liveDataStudent: LiveData<List<Student?>?>?

    init {
    
    
        myDatabase = MyDatabase.getInstance(application)
        liveDataStudent = myDatabase!!.studentDao()!!.getStudentList()
    }
}

然后,在 MainActivity 中初始化 List,实例化 StudentViewModel 并监听其 LiveData 的变化,代码如下:

package com.bignerdranch.android.jetpack5roomwithlivedataandviewmodeltest

import android.content.DialogInterface
import android.os.AsyncTask
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.widget.AdapterView.OnItemLongClickListener
import android.widget.EditText
import android.widget.ListView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import com.bignerdranch.android.jetpack5roomwithlivedataandviewmodeltest.database.MyDatabase
import com.bignerdranch.android.jetpack5roomwithlivedataandviewmodeltest.database.Student


class MainActivity : AppCompatActivity() {
    
    
    private var myDatabase: MyDatabase? = null
    private var studentList: MutableList<Student?>? = null
    private var studentAdapter: StudentAdapter? = null
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<View>(R.id.btnInsertStudent).setOnClickListener {
    
     openAddStudentDialog() }
        val lvStudent = findViewById<ListView>(R.id.lvStudent)
        studentList = ArrayList()
        studentAdapter = StudentAdapter(this@MainActivity, studentList)
        lvStudent.adapter = studentAdapter
        lvStudent.onItemLongClickListener = OnItemLongClickListener {
    
     parent, view, position, id ->
            updateOrDeleteDialog((studentList as ArrayList<Student?>)[position])
            false
        }
        myDatabase = MyDatabase.getInstance(this)
        val studentViewModel: StudentViewModel = ViewModelProvider(this)[StudentViewModel::class.java]
        studentViewModel.liveDataStudent!!.observe(this) {
    
     students ->
            (studentList as ArrayList<Student?>).clear()
            (studentList as ArrayList<Student?>).addAll(students!!)
            studentAdapter!!.notifyDataSetChanged()
        }
    }

    private fun updateOrDeleteDialog(student: Student?) {
    
    
        val options = arrayOf("更新", "删除")
        AlertDialog.Builder(this@MainActivity)
            .setTitle("")
            .setItems(options) {
    
     _, which ->
                if (which == 0) {
    
    
                    openUpdateStudentDialog(student)
                } else if (which == 1) {
    
    
                    if (student != null) {
    
    
                        DeleteStudentTask(student).execute()
                    }
                }
            }.show()
    }

    private fun openAddStudentDialog() {
    
    
        val customView: View = this.layoutInflater.inflate(R.layout.dialog_layout_student, null)
        val etName = customView.findViewById<EditText>(R.id.etName)
        val etAge = customView.findViewById<EditText>(R.id.etAge)
        val builder = AlertDialog.Builder(this@MainActivity)
        val dialog = builder.create()
        dialog.setTitle("Add Student")
        dialog.setButton(DialogInterface.BUTTON_POSITIVE, "OK") {
    
     dialog, which ->
            if (TextUtils.isEmpty(etName.text.toString()) || TextUtils.isEmpty(etAge.text.toString())) {
    
    
                Toast.makeText(this@MainActivity, "输入不能为空", Toast.LENGTH_SHORT).show()
            } else {
    
    
                InsertStudentTask(etName.text.toString(), etAge.text.toString()).execute()
            }
        }
        dialog.setButton(DialogInterface.BUTTON_NEGATIVE, "CANCEL") {
    
     dialog, which -> dialog.dismiss() }
        dialog.setView(customView)
        dialog.show()
    }

    private fun openUpdateStudentDialog(student: Student?) {
    
    
        if (student == null) {
    
    
            return
        }
        val customView: View = this.layoutInflater.inflate(R.layout.dialog_layout_student, null)
        val etName = customView.findViewById<EditText>(R.id.etName)
        val etAge = customView.findViewById<EditText>(R.id.etAge)
        etName.setText(student.name)
        etAge.setText(student.age)
        val builder = AlertDialog.Builder(this@MainActivity)
        val dialog = builder.create()
        dialog.setTitle("Update Student")
        dialog.setButton(DialogInterface.BUTTON_POSITIVE, "OK") {
    
     dialog, which ->
            if (TextUtils.isEmpty(etName.text.toString()) || TextUtils.isEmpty(etAge.text.toString())) {
    
    
                Toast.makeText(this@MainActivity, "输入不能为空", Toast.LENGTH_SHORT).show()
            } else {
    
    
                UpdateStudentTask(student.id, etName.text.toString(), etAge.text.toString()).execute()
            }
        }
        dialog.setButton(DialogInterface.BUTTON_NEGATIVE, "CANCEL") {
    
     dialog, which -> dialog.dismiss() }
        dialog.setView(customView)
        dialog.show()
    }

    private inner class InsertStudentTask(var name: String, var age: String) :
        AsyncTask<Void?, Void?, Void?>() {
    
    
        override fun doInBackground(vararg params: Void?): Void? {
    
    
            myDatabase!!.studentDao()!!.insertStudent(Student(name, age))
            return null
        }
    }

    private inner class UpdateStudentTask(var id: Int, var name: String, var age: String) :
        AsyncTask<Void?, Void?, Void?>() {
    
    
        override fun doInBackground(vararg params: Void?): Void? {
    
    
            myDatabase!!.studentDao()!!.updateStudent(Student(id, name, age))
            return null
        }
    }

    private inner class DeleteStudentTask(var student: Student) : AsyncTask<Void?, Void?, Void?>() {
    
    
        override fun doInBackground(vararg params: Void?): Void? {
    
    
            myDatabase!!.studentDao()!!.deleteStudent(student)
            return null
        }
    }
}

运行后,当 LiveData 数据变化时,更新 UI 即可,而不需要每次增删改后都必须用 QueryStudentTask() 来手动查询一次数据库,简化了系统,效果如下:

在这里插入图片描述

5.3 Room 数据库 version 升级

5.3.1 用 Migration 升级

在 MyDatabase.kt 中用 Migration 升级数据库,代码如下:

@Database(entities = [Student::class], exportSchema = false, version = 1)
abstract class MyDatabase() : RoomDatabase() {
    
    
    abstract fun studentDao(): StudentDao?

    companion object {
    
    
        private val DATABASE_NAME = "my_db"
        private var databaseInstance: MyDatabase? = null
        @Synchronized
        fun getInstance(context: Context): MyDatabase? {
    
    
            if (databaseInstance == null) {
    
    
                databaseInstance = Room
                    .databaseBuilder(context.applicationContext, MyDatabase::class.java, DATABASE_NAME)
                    .fallbackToDestructiveMigration()
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_1_3, MIGRATION_3_4)
                    .build()
            }
            return databaseInstance
        }

        val MIGRATION_1_2: Migration = object : Migration(1, 2) {
    
    
            override fun migrate(database: SupportSQLiteDatabase) {
    
    
                //do something
                Log.d("MyDatabase", "MIGRATION_1_2")
            }
        }
        private val MIGRATION_2_3: Migration = object : Migration(2, 3) {
    
    
            override fun migrate(database: SupportSQLiteDatabase) {
    
    
                //do something
                Log.d("MyDatabase", "MIGRATION_2_3")
            }
        }
        private val MIGRATION_1_3: Migration = object : Migration(1, 3) {
    
    
            override fun migrate(database: SupportSQLiteDatabase) {
    
    
                //do something
                Log.d("MyDatabase", "MIGRATION_1_3")
            }
        }
        val MIGRATION_3_4: Migration = object : Migration(3, 4) {
    
    
            override fun migrate(database: SupportSQLiteDatabase) {
    
    
                Log.d("MyDatabase", "MIGRATION_3_4")
                database.execSQL(
                    "CREATE TABLE temp_Student (" +
                            "id INTEGER PRIMARY KEY NOT NULL," +
                            "name TEXT," +
                            "age TEXT)"
                )
                database.execSQL(
                    "INSERT INTO temp_Student (id, name, age) " +
                            "SELECT id, name, age FROM Student"
                )
                database.execSQL("DROP TABLE Student")
                database.execSQL("ALTER TABLE temp_Student RENAME TO Student")
            }
        }
    }
}

然后,通过 .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_1_3, MIGRATION_3_4) 执行升级。通过 @Database(entities = [Student::class], version = 1) 可标记数据库的目标 verison。用 fallbackToDestructiveMigration() 避免升级失败的崩溃。

对于跨 version 的情况,例如如果当前手机设备上 sqlite 的数据库 version 是 1,要安装的 App 的数据库 version 是3,Room 会先找 Migration(1,3),若无则执行 Migration(1,2) 和 Migration(2,3)

5.3.2 export 各版本数据库的 Sechema 文件

schema 文件记录了数据库的变化,方便我们了解历次数据库操作是否成功。

在 build.gradle(app) 中指定 Schema 文件导出的位置,设置如下:

android {
    
    
    defaultConfig {
    
    
        javaCompileOptions {
    
    
            annotationProcessorOptions {
    
    
                arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
            }
        }
    }
}

并在 MyDatabase.kt 中设置 exportSchema = true( @Database(entities = [Student::class], exportSchema = true, version = 2))。

运行后,即可在 app/schemas 下看到导出的数据库json文件,效果如下:

在这里插入图片描述

5.3.3 修改表结构:销毁旧表并重建新表

在 sqlite 修改表结构,例如把 Student 表的 age 字段从 INTEGET 改为 TEXT,需要销毁旧表并重建新表,分为如下步骤:

  1. 创建临时表 tmpStudent
  2. 将数据从旧表 Student 复制到临时表 tmpStudent
  3. 删除旧表 Student
  4. 将临时表 tmpStudent 重命名为 Student

MyDatabase.kt 的代码如下:

        val MIGRATION_3_4: Migration = object : Migration(3, 4) {
    
    
            override fun migrate(database: SupportSQLiteDatabase) {
    
    
                Log.d("MyDatabase", "MIGRATION_3_4")
                database.execSQL(
                    "CREATE TABLE temp_Student (" +
                            "id INTEGER PRIMARY KEY NOT NULL," +
                            "name TEXT," +
                            "age TEXT)"
                )
                database.execSQL(
                    "INSERT INTO temp_Student (id, name, age) " +
                            "SELECT id, name, age FROM Student"
                )
                database.execSQL("DROP TABLE Student")
                database.execSQL("ALTER TABLE temp_Student RENAME TO Student")
            }
        }

运行后,数据库字段类型变化生效,效果如下:

在这里插入图片描述

5.4 用 createFromAsset() 和 createFromFile() 预填充数据库

首先,创建一个 student 的数据库,示例如下:

bash > sqlite3 student.db
sqlite> create table student(id integer not null, name text not null, age text not null, primary key(id autoincrement));
sqlite> insert into student values (1,'zhao','1'),(2,'qian','2'),(3,'sun','3');

设置 Student.kt 都为 NotNull,与上文的 student.db 的数据类型匹配,代码如下:

@Entity(tableName = "student")
class Student {
    
    
    @NotNull
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
    var id = 0

    @NotNull
    @ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
    var name: String

    @NotNull
    @ColumnInfo(name = "age", typeAffinity = ColumnInfo.TEXT)
    var age: String

    /**
     * Room会使用这个构造器来存储数据,也就是当你从表中得到Student对象时候,Room会使用这个构造器
     */
    constructor(id: Int, name: String, age: String) {
    
    
        this.id = id
        this.name = name
        this.age = age
    }

    /**
     * 由于Room只能识别和使用一个构造器,如果希望定义多个构造器,你可以使用Ignore标签,让Room忽略这个构造器
     * 同样,@Ignore标签还可用于字段,使用@Ignore标签标记过的字段,Room不会持久化该字段的数据
     */
    @Ignore
    constructor(name: String, age: String) {
    
    
        this.name = name
        this.age = age
    }
}

其次,新建 assets folder 目录,示例如下:

在这里插入图片描述

然后,新建 app/assets/databases,并在其中放置 student.db 文件,其位置如下:

在这里插入图片描述

然后,在 MyDatabase.kt 中,用 createFromAsset() 方法从 assets/database/students.db 创建 Room 数据库,代码如下:

package com.bignerdranch.android.jetpack5roomwithlivedataandviewmodeltest.database

import android.content.Context
import android.util.Log
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase


@Database(entities = [Student::class], exportSchema = false, version = 1)
abstract class MyDatabase() : RoomDatabase() {
    
    
    abstract fun studentDao(): StudentDao?

    companion object {
    
    
        private val DATABASE_NAME = "my_db"
        private var databaseInstance: MyDatabase? = null

        @Synchronized
        fun getInstance(context: Context): MyDatabase? {
    
    
            if (databaseInstance == null) {
    
    
                databaseInstance = Room
                    .databaseBuilder(context.applicationContext, MyDatabase::class.java, DATABASE_NAME)
                    .createFromAsset("databases/student.db")
                    .fallbackToDestructiveMigration()
//                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_1_3, MIGRATION_3_4)
                    .build()
            }
            return databaseInstance
        }

        val MIGRATION_1_2: Migration = object : Migration(1, 2) {
    
    
            override fun migrate(database: SupportSQLiteDatabase) {
    
    
                //do something
                Log.d("MyDatabase", "MIGRATION_1_2")
            }
        }
        private val MIGRATION_2_3: Migration = object : Migration(2, 3) {
    
    
            override fun migrate(database: SupportSQLiteDatabase) {
    
    
                //do something
                Log.d("MyDatabase", "MIGRATION_2_3")
            }
        }
        private val MIGRATION_1_3: Migration = object : Migration(1, 3) {
    
    
            override fun migrate(database: SupportSQLiteDatabase) {
    
    
                //do something
                Log.d("MyDatabase", "MIGRATION_1_3")
            }
        }
        val MIGRATION_3_4: Migration = object : Migration(3, 4) {
    
    
            override fun migrate(database: SupportSQLiteDatabase) {
    
    
                Log.d("MyDatabase", "MIGRATION_3_4")
                database.execSQL(
                    "CREATE TABLE temp_Student (" +
                            "id INTEGER PRIMARY KEY NOT NULL," +
                            "name TEXT," +
                            "age TEXT)"
                )
                database.execSQL(
                    "INSERT INTO temp_Student (id, name, age) " +
                            "SELECT id, name, age FROM Student"
                )
                database.execSQL("DROP TABLE Student")
                database.execSQL("ALTER TABLE temp_Student RENAME TO Student")
            }
        }
    }
}

运行后,从指定的 student.db 文件创建了数据库,示例如下:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/jiaoyangwm/article/details/127076643