我写dcache数据库版本升级的心得

178 阅读9分钟

前言

写这个项目最早要追溯到2021年,github.com/dora4/dcach… 。那个时候就想把设计模式给熟练一下,自己写一个涵盖ORM数据库层、网络请求层和属于这两层的中间层的缓存层的框架。刚开始的时候先从最基础的ORM开始一行一行的实现,一点一滴的积累。当时也参考了一些其他的ORM框架,比如OrmLite等。后面基本就是结合自己所学的设计模式和框架设计的那一套理论,慢慢打磨。每天下班空闲时间就写一点点,当时还没提交到Github。每天虽然写的不多,但贵在坚持,最终总算把基本架子搭起来了,但是还有很多细节问题要处理。后来就先放一边,先去写网络请求了,网络请求的实现之路要顺利得多,可能是因为市面上的网络请求库都比较成熟,且代码量本身就不大的缘故。再后来就是重头戏,缓存层的实现了。这块的实现我可是绞尽了脑汁,特别是分页缓存的设计上,差点让我放弃治疗。缓存层的实现应用了相当多的设计模式,这些都是对每一种设计模式都理解后对代码结构的,起码我认为的最佳选择。你要设计整体的架构,还真要把每一种设计模式的作用用途、使用场景和优缺点都啃透,这样才能做到不刻意使用设计模式,但就觉得应该这么设计。

我就比较重要的设计进行思路讲解。我跟你说,这个库里面的每一行代码都是仔细斟酌写出来的,写一行代码,可能我要反复修改无数次,包括访问控制符、空检查、类和方法命名怎样体现单一职责原则,对,要做到你一个标点一个空格都不要改。

数据库版本升级(数据迁移)

package dora.db.migration

import dora.db.dao.OrmDao
import dora.db.table.OrmTable
import java.io.Serializable

/**
 * Used to define all operations during data migration when upgrading the database version. For
 * example, val MIGRATION_1_2 = OrmMigration(1, 2).
 * 简体中文:用于定义数据库版本升级时,数据迁移时的所有操作。例如val MIGRATION_1_2 = OrmMigration(1, 2)。
 *
 * @see OrmTable
 */
open class OrmMigration(val fromVersion: Int, val toVersion: Int) : Serializable {

    /**
     * Used when upgrading by a single version only.
     * 简体中文:只升一个版本时使用。
     */
    constructor(fromVersion: Int) : this(fromVersion, fromVersion + 1)

    /**
     * @see [dora.db.table.TableManager.createTable]
     * @see [dora.db.dao.OrmDao.renameTable]
     * @see [dora.db.dao.OrmDao.addColumn]
     * @see [dora.db.dao.OrmDao.renameColumn]
     */
    open fun migrate(dao: OrmDao<out OrmTable>) : Boolean {
        return false
    }
}
表结构升级的思路

SQLite的SQL语句和MySQL还是有些差别的,可以说是mini版,所以有些MySQL的SQL语句在SQLite是用不了的,而我的ORM框架是基于Android内置SQLite的。在数据库版本号提升的时候,你应该把所有表结构变化和影响到的数据都要进行处理。SQLite的SQL语句可以执行ALTER TABLE ADD COLUMN语句,但是不能执行ALTER TABLE SET COLUMN。所以说你要修改一个列的约束,还是一个比较麻烦的事,你要新建一个列,然后把原来这个列上的数据迁移过来,改个列名。在OrmTable的实现类中会指定OrmMigration的数组,也就是每一个版本所有表结构升级的变化都会保存在这里。主要就是要重写migrate方法进行数据的操作。那么这个dao是怎么来的呢?

OrmDao从何而来?

这个OrmDao为什么在升级的时候就可以用了?我们带着问题看源码。我们知道Android的数据库操作都离不开一个类,SQLiteOpenHelper。这个类的onCreate()和onUpgrade()分别在数据库创建和升级的时候被回调。

package dora.db

import android.content.Context
import android.database.DatabaseErrorHandler
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import dora.db.exception.OrmMigrationException
import dora.db.migration.OrmMigration
import dora.db.table.OrmTable
import dora.db.table.TableManager
import java.lang.reflect.InvocationTargetException

/**
 * A helper class to manage database creation and version management.You need to create all tables
 * in the [Orm.init] function and it is not recommended to create tables using [TableManager].
 * If you need to upgrade the table structure, use an array of [OrmMigration] to specify each
 * version change.
 * 简体中文:一个帮助类,用于管理数据库的创建和版本管理。你需要在[Orm.init]函数中创建所有表,不建议使用
 * [TableManager]自行创建表。如果你需要升级数据的表结构,需要使用[OrmMigration]的数组指定每一次版本的变动。
 */
class OrmSQLiteOpenHelper(private val context: Context, name: String, version: Int,
                          private val tables: Array<Class<out OrmTable>>?) :
        SQLiteOpenHelper(context, name, null, version, DatabaseErrorHandler {
            dbObj -> OrmLog.e(dbObj.toString()) }) {

    override fun onCreate(db: SQLiteDatabase) {
        if (!tables.isNullOrEmpty()) {
            for (table in tables) {
                TableManager.createTable(table)
            }
        }
    }

    private fun <T> newOrmTableInstance(clazz: Class<T>): T? {
        val constructors = clazz.declaredConstructors
        for (c in constructors) {
            c.isAccessible = true
            val cls = c.parameterTypes
            if (cls.isEmpty()) {
                try {
                    return c.newInstance() as T
                } catch (e: InstantiationException) {
                    e.printStackTrace()
                } catch (e: IllegalAccessException) {
                    e.printStackTrace()
                } catch (e: InvocationTargetException) {
                    e.printStackTrace()
                }
            } else {
                val objs = arrayOfNulls<Any>(cls.size)
                for (i in cls.indices) {
                    objs[i] = getPrimitiveDefaultValue(cls[i])
                }
                try {
                    return c.newInstance(*objs) as T
                } catch (e: InstantiationException) {
                    e.printStackTrace()
                } catch (e: IllegalAccessException) {
                    e.printStackTrace()
                } catch (e: InvocationTargetException) {
                    e.printStackTrace()
                }
            }
        }
        return null
    }

    private fun getPrimitiveDefaultValue(clazz: Class<*>): Any? {
        return if (clazz.isPrimitive) {
            if (clazz == Boolean::class.javaPrimitiveType) false else 0
        } else null
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        if (!tables.isNullOrEmpty() && newVersion > oldVersion) {
            for (i in tables.indices) {
                val table = tables[i]
                val ormTable: OrmTable? = newOrmTableInstance(tables[i])
                ormTable?.let {
                    val isRecreated = it.isUpgradeRecreated
                    if (isRecreated) {
                        Transaction.execute(db) {
                            TableManager.dropTable(table)
                            TableManager.createTable(table)
                        }
                    } else {
                        var curVersion = oldVersion
                        if (it.migrations == null) {
                            return@let
                        }
                        for (migration in it.migrations!!) {
                            if (migration.fromVersion >= migration.toVersion) {
                                throw OrmMigrationException(
                                    "fromVersion can't be more than toVersion," +
                                            "either fromVersion can't be equal to toVersion."
                                )
                            }
                            if (migration.toVersion <= curVersion) {
                                continue
                            }
                            if (migration.fromVersion == curVersion) {
                                try {
                                    Transaction.execute(db, it.javaClass as Class<out OrmTable>) { dao ->
                                        migration.migrate(dao)
                                    }
                                    curVersion = migration.toVersion
                                    OrmLog.d("${it.javaClass.name}'s version has succeeded upgraded to $curVersion")
                                } catch (e: Exception) {
                                    OrmLog.e(e.toString())
                                    throw OrmMigrationException("${it.javaClass.name}'s version failed to upgrade to $curVersion")
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

所以说我们应该在onUpgrade回调中处理数据表结构升级的逻辑。这个类是在Application创建的时候被初始化的。

package dora.db

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import dora.db.exception.OrmStateException
import dora.db.table.OrmTable

/**
 * Use it to configure the ORM framework.Call [Orm.init] during application initialization to
 * complete the configuration.
 * 简体中文:使用它对ORM框架进行配置。在应用初始化时调用[Orm.init]即可完成配置。
 */
object Orm {

    private var database: SQLiteDatabase? = null
    private var dbHelper: SQLiteOpenHelper? = null
    private const val STATE_DATABASE_NOT_EXISTS = -1
    private const val STATE_DATABASE_EXISTS = 0
    private var dbState = STATE_DATABASE_NOT_EXISTS

    @Synchronized
    fun init(context: Context, databaseName: String) {
        prepare(OrmSQLiteOpenHelper(context, databaseName, 1, null))
    }

    @Synchronized
    fun init(context: Context, config: OrmConfig) {
        val name: String = config.databaseName
        val versionCode: Int = config.versionCode
        val tables: Array<Class<out OrmTable>>? = config.tables
        prepare(context, name, versionCode, tables)
    }

    fun getDB() : SQLiteDatabase {
        if (isPrepared()) {
            return database!!
        }
        throw OrmStateException("Database is not exists.")
    }

    private fun prepare(helper: OrmSQLiteOpenHelper) {
        dbHelper = helper
        database = helper.writableDatabase
        dbState = STATE_DATABASE_EXISTS
    }

    private fun prepare(
        context: Context,
        name: String,
        versionCode: Int,
        tables: Array<Class<out OrmTable>>?
    ) {
        dbHelper = OrmSQLiteOpenHelper(context, name, versionCode, tables)
        prepare(dbHelper as OrmSQLiteOpenHelper)
    }

    fun isPrepared() : Boolean {
        return dbState == STATE_DATABASE_EXISTS
    }
}

你要用Orm.init有两种方式,一种是调用默认的init,一种是调用自定义配置的init。通常我们都是调用自定义配置的init。init里面会调用prepare方法进行OrmSQLiteOpenHelper的创建。并把SQLiteDatabase对象保存起来,这个东西很关键。如果你没调用init进行初始化,或者初始化失败了,你是无法通过getDB方法拿到这个database对象的。而OrmDao对database的依赖是强依赖,也就是类的组合关系,而非聚合关系,相当于手对人来说是原生的,而手套这个是可有可无的。

为什么说有时我们不能去碰Transaction.db

我们再来看事务操作的实现。

package dora.db

import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import dora.db.dao.DaoFactory
import dora.db.dao.OrmDao
import dora.db.table.OrmTable

/**
 * Transaction operations ensure that multiple database actions either all succeed or, if any fail,
 * the system rolls back to the initial state.
 * 简体中文:事务操作,使多个数据库操作要么全部成功,要么则回滚至最初状态。
 */
object Transaction {

    /**
     * The [android.database.sqlite.SQLiteDatabase] object can be directly used in the
     * [execute] function.
     * 简体中文:[android.database.sqlite.SQLiteDatabase]对象,可以在[execute]函数中直接使用。
     */
    val db: SQLiteDatabase
        get() = Orm.getDB()

    /**
     * Execute a general transaction block.
     * 简体中文:执行通用事务块。
     */
    internal fun <T> execute(db: SQLiteDatabase, block: Transaction.() -> T) : Any = apply {
        try {
            // Begin the transaction.
            // 简体中文:开始事务
            db.beginTransaction()
            // Execute the transaction operation.
            // 简体中文:执行事务操作
            block()
            // Set the flag indicating that all operations were executed successfully.
            // 简体中文:设置所有操作执行成功的标志位
            db.setTransactionSuccessful()
        } catch (e: SQLiteException) {
            e.printStackTrace()
        } finally {
            // End the transaction.
            // 简体中文:结束事务
            db.endTransaction()
        }
    }

    /**
     * Execute a general transaction block.
     * 简体中文:执行通用事务块。
     */
    fun <T> execute(block: Transaction.() -> T) : Any = apply {
        try {
            // Begin the transaction.
            // 简体中文:开始事务
            db.beginTransaction()
            // Execute the transaction operation.
            // 简体中文:执行事务操作
            block()
            // Set the flag indicating that all operations were executed successfully.
            // 简体中文:设置所有操作执行成功的标志位
            db.setTransactionSuccessful()
        } catch (e: SQLiteException) {
            e.printStackTrace()
        } finally {
            // End the transaction.
            // 简体中文:结束事务
            db.endTransaction()
        }
    }

    /**
     * Execute a single-table transaction block.
     * 简体中文:执行单表事务块。
     */
    internal fun <T : OrmTable> execute(db: SQLiteDatabase, tableClass: Class<T>, block: Transaction.(dao: OrmDao<T>) -> Unit) :
            Any = apply {
        val dao = DaoFactory.getDao(tableClass, db)
        try {
            // Begin the transaction.
            // 简体中文:开始事务
            db.beginTransaction()
            // Execute the transaction operation.
            // 简体中文:执行事务操作
            block(dao)
            // Set the flag indicating that all operations were executed successfully.
            // 简体中文:设置所有操作执行成功的标志位
            db.setTransactionSuccessful()
        } catch (e: SQLiteException) {
            e.printStackTrace()
        } finally {
            // End the transaction.
            // 简体中文:结束事务
            db.endTransaction()
        }
    }

    /**
     * Execute a single-table transaction block.
     * 简体中文:执行单表事务块。
     */
    fun <T : OrmTable> execute(tableClass: Class<T>, block: Transaction.(dao: OrmDao<T>) -> Unit) :
            Any = apply {
        val dao = DaoFactory.getDao(tableClass)
        try {
            // Begin the transaction.
            // 简体中文:开始事务
            db.beginTransaction()
            // Execute the transaction operation.
            // 简体中文:执行事务操作
            block(dao)
            // Set the flag indicating that all operations were executed successfully.
            // 简体中文:设置所有操作执行成功的标志位
            db.setTransactionSuccessful()
        } catch (e: SQLiteException) {
            e.printStackTrace()
        } finally {
            // End the transaction.
            // 简体中文:结束事务
            db.endTransaction()
        }
    }
}

“get() =”这个玩意儿你真的理解吗?它和“by lazy”有什么区别?首先这两个都是延迟加载的方式,也就是你不访问这个变量,是不会执行后面的计算代码的。它们的区别在于“by lazy”会更注重结果,也就是只有第一次才计算,后面都是拿的缓存。而“get()=”每次都会重新计算。为什么我们这里需要每次都重新计算?我们再回过头来看Orm类的代码,要先检测数据库是否准备好对吧!那么这个状态肯定是每次都要拿最新的啊。有一种情况我们是不能让它展开计算的,要不然肯定会抛这个异常throw OrmStateException("Database is not exists.")。如果使用的Transaction的execute方法用的是Transaction的成员属性,那么这个异常必定会被抛出来。在OrmSQLiteOpenHelper类的onUpgrade方法中我们能确保数据库是准备好的状态吗?不能。只有调用了prepare方法后才会给database赋值。那么这里我们就只能使用onUpgrade方法中的database参数对象进行操作,因为我们不能去展开计算那个getDB()。所以在onUpgrade方法中使用的execute都是带internal的,框架专用的,不给使用者使用。我们再来看OrmDao的构造方法定义。

class OrmDao<T : OrmTable> internal @JvmOverloads constructor(
    private val beanClass: Class<T>,
    db: SQLiteDatabase? = null) : Dao<T> {

    val database: SQLiteDatabase = db ?: Orm.getDB()
}

使用@JvmOverloads注解生成重载方法。原先的beanClass参数不动,增加一种可以使用自己的db对象的方式。当然,你不传,我们原先的逻辑还是保持原样,使用Orm.getDB()的database对象。由于kotlin @JvmOverloads注解重载的机制,我们不能把逻辑写在里面,如果不调用两个参数的构造函数,那么Orm.getDB()的这块逻辑就不会被执行,导致database对象永远都是空,那么这个OrmDao对象也就废了。

为什么要定义一个Transaction的成员变量db?

既然你搞了这么大一个坑,为什么要去定义Transaction的成员变量db呢?那肯定是有好处的啊,看fun <T> execute(db: SQLiteDatabase, block: Transaction.() -> T) : Any = apply这个高阶函数的定义,我们可以在调用execute高阶函数的代码块中直接使用Transaction的db对象,这样就很方便执行sql语句了,而不用使用封装过的OrmDao来操作表了。