SQLite是一个嵌入app的轻量级数据库,Android使用SQLite作为它的数据库管理系统。 在不使用第三方数据操作框架如GreenDao等的情况下,我们操作Android的操作数据库 的API在Android中是非常原生的。需要编写很多诸如增删除改查的SQL语句以及对象与ContentValues或者Cursors之间的解析处理。现在使用Kotlin和Anko,我们可以大量简化这些。
–ManagedSqliteOpenHelper——–
Anko提供了强大的SqliteOpenHelper来简化开发代码。通常使用一个SqliteOpenHelper时,需要去调用getReadableDatabase()或者getWritableDatabase(),然后执行相应操作拿到结果。使用之后不能忘记调用close()。使用ManagedSqliteOpenHelper我们只需要:
forecastDbHelper.use {
...
}
在lambda里面,可以直接使用SqliteDatabase中的函数。
public fun <T> use(f: SQLiteDatabase.() -> T): T {
try {
return openDatabase().f()
} finally {
closeDatabase()
}
}
首先,use接收一个SQLiteDatabase的扩展函数。这表示,可以使用this在大括号中,并且处于SQLiteDatabase对象中。 这个函数扩展可以返回一个值,所以我们可以像这么做:
val result = forecastDbHelper.use {
val queriedObject = ...
queriedObject
}
在一个函数中,最后一行表示返回值。因为T没有任何的限制,所以我们可以返回任何对象。甚至如果我们不想返回任何值就使用Unit。 使用了try-finall,use方法会确保不管在数据库操作执行成功还是失败都会去关闭数据库。
–定义表结构字段——–
创建objects可以让我们避免表名列名拼写错误、重复等。例子中需要两个表: 一个用来保存城市的信息,另一个用来保存某天的天气预报。 第二张表会有一个关联到第一张表的字段。 CityForecastTable提供了表的名字还有需要列:一个id(这个城市的zipCode),城市的名称和所在国家。
object CityForecastTable { val NAME = "CityForecast" val ID = "_id" val CITY = "city" val COUNTRY = "country" }
DayForecast有更多的信息,就如你下面看到的有很多的列。最后一列cityId,用来保持属于的城市id。
object DayForecastTable { val NAME = "DayForecast" val ID = "_id" val DATE = "date" val DESCRIPT ION = "descript ion" val HIGH = "high" val LOW = "low" val ICON_URL = "iconUrl" val CITY_ID = "cityId" }
–实现SqliteOpenHelper——–
SqliteOpenHelper的作用就是数据库的创建和更新,并提供了一个SqliteDatebase,可以用它来实现我们想要的目的。
class ForecastDbHelper() : ManagedSQLiteOpenHelper(App.instance,
ForecastDbHelper.DB_NAME, null, ForecastDbHelper.DB_VERSION) {
...
}
前面的章节中使用过我们创建的App.instance,这次我们同样的包括数据库名称和版本。这些值我们都会与SqliteOpenHelper一起定义在companion object中:
companion object { val DB_NAME = "forecast.db" val DB_VERSION = 1 val instance: ForecastDbHelper by lazy { ForecastDbHelper() } }
instance这个属性使用了lazy委托,表示直到它真的被调用才会被创建。用这种方法,如果数据库从来没有被使用,没有必要去创建这个对象。 一般lazy委托的代码块可以阻止在多个不同的线程中创建多个对象。这个只会发生在两个线程在同事时间访问这个instance对象, 它很难发生但是发生具体还有看app的实现。无人如何,lazy委托是线程安全的。 创建这些定义的表,需要去提供一个onCreate函数的实现。当没有库使用的时候,创建表会通过写原生的包含定义好的列和类型的CREATE TABLE语句来实现。 然而Anko提供了一个简单的扩展函数,它接收一个表的名字和一系列由列名和类型构建的Pair对象:
db.createTable(CityForecastTable.NAME, true, Pair(CityForecastTable.ID, INTEGER + PRIMARY_KEY), Pair(CityForecastTable.CITY, TEXT), Pair(CityForecastTable.COUNTRY, TEXT))
第一个参数是表的名称 第二个参数,当是true的时候,创建之前会检查这个表是否存在。 第三个参数是一个Pair类型的vararg参数。vararg也存在在Java中,这是一种在一个函数中传入联系很多相同类型的参数。这个函数也接收一个对象数组。 Anko中有一种叫做SqlType的特殊类型,它可以与SqlTypeModifiers混合,比如PRIMARY_KEY。+操作符像之前那样被重写了。 这个plus函数会把两者通过合适的方式结合起来,然后返回一个新的SqlType:
fun SqlType.plus(m: SqlTypeModifier) : SqlType { return SqlTypeImpl(name, if (modifier == null) m.toString() else "$modifier $m") }
它会把多个修饰符组合起来。
当然我们可以修改得更好。Kotlin标准库中包含了一个叫to的函数,又一次,让我们来展示Kotlin的强大之处。 它作为第一参数的扩展函数,接收另外一个对象作为参数,把两者组装并返回一个Pair。
public fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
因为带有一个函数参数的函数可以被用于inline,所以结果非常清晰:
val pair = object1 to object2
然后,把他们应用到表的创建中:
db.createTable(CityForecastTable.NAME, true, CityForecastTable.ID to INTEGER + PRIMARY_KEY, CityForecastTable.CITY to TEXT, CityForecastTable.COUNTRY to TEXT)
这就是整个函数看起来的样子:
override fun onCreate(db: SQLiteDatabase) { db.createTable(CityForecastTable.NAME, true, CityForecastTable.ID to INTEGER + PRIMARY_KEY, CityForecastTable.CITY to TEXT, CityForecastTable.COUNTRY to TEXT) db.createTable(DayForecastTable.NAME, true, DayForecastTable.ID to INTEGER + PRIMARY_KEY + AUTOINCREMENT, DayForecastTable.DATE to INTEGER, DayForecastTable.DESCRIPT ION to TEXT, DayForecastTable.HIGH to INTEGER, DayForecastTable.LOW to INTEGER, DayForecastTable.ICON_URL to TEXT, DayForecastTable.CITY_ID to INTEGER) }
onUpgrade将只是删除表,然后重建它们。我们只是把我们数据库作为一个缓存,所以这是一个简单安全的方法保证我们的表会如我们所期望的那样被重建。 如果我有很重要的数据需要保留,我们就需要优化onUpgrade的代码,让它根据数据库版本来做相应的数据转移。
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.dropTable(CityForecastTable.NAME, true) db.dropTable(DayForecastTable.NAME, true) onCreate(db) }
–创建数据库model类——–
接下来为数据库创建model类。作用之前所见的map委托的方式来把这些属性直接映射到数据库中,反过来也一样。 我们先来看下CityForecast类:
class CityForecast(val map: MutableMap<String, Any?>,
val dailyForecast: List<DayForecast>) {
var _id: Long by map
var city: String by map
var country: String by map
constructor(id: Long, city: String, country: String,
dailyForecast: List<DayForecast>)
: this(HashMap(), dailyForecast) {
this._id = id
this.city = city
this.country = country
}
}
默认的构造函数会得到一个含有属性和对应的值的map,和一个dailyForecast。 多亏了委托,这些值会根据key的名字会映射到相应的属性中去。 如果希望映射的过程运行完美,那么属性的名字必须要和数据库中对应的名字一模一样。 第二个构造函数也是必要的。因为需要从domain映射到数据库类中,所以不能使用map,从属性中设置值也是方便的。 传入一个空的map,但是又一次,多亏了委托,当我们设置值到属性的时候,它会自动增加所有的值到map中。 用这种方式,我们就准备好map来保存到数据库中了。使用了这些有用的代码,我将会看见它运行起来就像魔法一样神奇。 第二个类,DayForecast,它会是第二个表。它包括表中的每一列作为它的属性,它也有第二个构造函数。 唯一不同之处就是不需要设置id,因为它将通过SQLite自增长。
class DayForecast(var map: MutableMap<String, Any?>) { var _id: Long by map var date: Long by map var descript ion: String by map var high: Int by map var low: Int by map var iconUrl: String by map var cityId: Long by map constructor(date: Long, descript ion: String, high: Int, low: Int, iconUrl: String, cityId: Long) : this(HashMap()) { this.date = date this.descript ion = descript ion this.high = high this.low = low this.iconUrl = iconUrl this.cityId = cityId } }
这些类将会帮助我们SQLite表与对象之间的互相映射。
–写入和查询数据库——–
SqliteOpenHelper只是一个工具,是SQL世界和OOP之间的一个通道。还需要新建几个类来请求已经保存在数据库中的数据,和保存新的数据。 被定义的类会使用ForecastDbHelper和DataMapper来转换数据库中的数据到domain models。我仍旧使用默认值的方式来实现简单的依赖注入:
class ForecastDb(
val forecastDbHelper: ForecastDbHelper = ForecastDbHelper.instance,
val dataMapper: DbDataMapper = DbDataMapper()) {
...
}
所有的函数使用前面章节讲到过的use()函数。lambda返回的值也会被作为这个函数的返回值。所以定义一个使用zip code和date来查询一个forecast的函数:
fun requestForecastByZipCode(zipCode: Long, date: Long) = forecastDbHelper.use {
...
}
我们使用use函数返回的结果作为这个函数返回的结果。
查询一个forecast:第一个要做的查询就是每日的天气预报,因为我们需要这个列表来创建一个city对象。 Anko提供了一个简单的请求构建器,所以我们来利用下这个有利条件:
val dailyRequest = "${DayForecastTable.CITY_ID} = ? " + "AND ${DayForecastTable.DATE} >= ?" val dailyForecast = select(DayForecastTable.NAME) .whereSimple(dailyRequest, zipCode.toString(), date.toString()) .parseList { DayForecast(HashMap(it)) }
第一行,dailyRequest是查询语句中where的一部分。它是whereSimple函数需要的第一个参数,这与我们用一般的helper做的方式很相似。 这里有另外一个简化的where函数,它需要一些tags和values来进行匹配。 我不太喜欢这个方式,因为我觉得这个增加了代码的模版化,虽然这个对我们把values解析成String很有利。最后它看起来会是这样:
val dailyRequest = "${DayForecastTable.CITY_ID} = {id}" + "AND ${DayForecastTable.DATE} >= {date}" val dailyForecast = select(DayForecastTable.NAME) .where(dailyRequest, "id" to zipCode, "date" to date) .parseList { DayForecast(HashMap(it)) }
你可以选择你喜欢的一种方式。select函数是很简单的,它仅仅是需要一个被查询表的名字。parse函数的时候会有一些魔法在里面。 在这个例子中我们假设请求结果是一个list,使用了parseList函数。它使用了RowParser或RapRowParser函数去把cursor转换成一个对象的集合。 这两个不同之处就是RowParser是依赖列的顺序的,而MapRowParser是从map中拿到作为column的key名的。 它们之间有两个重载的冲突,所以不能直接使用简化的方式准确地创建需要的对象。但是没有什么是不能通过扩展函数来解决的。 创建了一个接收一个lambda函数返回一个MapRowParser的函数。解析器会调用这个lambda来创建这个对象:
fun <T : Any> SelectQueryBuilder.parseList( parser: (Map<String, Any>) -> T): List<T> = parseList(object : MapRowParser<T> { override fun parseRow(columns: Map<String, Any>): T = parser(columns) })
这个函数可以帮助我们简单地去parseList查询的结果:
parseList { DayForecast(HashMap(it)) }
解析器接收的immutable map被我们转化成了一个mutable map(我们需要在database model中是可以修改的)通过使用相应的HashMap构造函数。 在DayForecast中的构造函数中会使用到这个HashMap。 所以,这个查询返回了一个Cursor,要理解这个场景的背后到底发生了什么。 parseList中会迭代它,然后得到Cursor的每一行直到最后一个。 对于每一行,它会创建一个包含这列的key和给对应的key赋值后的map。然后把这个map返回给这个解析器。 如果查询没有任何结果,parseList会返回一个空的list。 下一步查询城市也是一样的方法:
val city = select(CityForecastTable.NAME) .whereSimple("${CityForecastTable.ID} = ?", zipCode.toString()) .parseOpt { CityForecast(HashMap(it), dailyForecast) }
不同之处是:我们使用的是parseOpt。这个函数返回一个可null的对象。结果可以使一个null或者单个的对象,这取决于请求是否能在数据库中查询到数据。 这里有另外一个叫parseSingle的函数,本质上是一样的,但是它返回的事一个不可null的对象。 所以如果没有在数据库中找到这一条数据,它会抛出一个异常。在我们的例子中,第一次查询一个城市的时候,肯定是不存在的,所以使用parseOpt会更安全。 我又创建了一个好用的函数来阻止我们需要的对象的创建:
public fun <T : Any> SelectQueryBuilder.parseOpt( parser: (Map<String, Any>) -> T): T? = parseOpt(object : MapRowParser<T> { override fun parseRow(columns: Map<String, Any>): T = parser(columns) })
最后如果返回的city不是null,我们使用dataMapper把它转换成domain object再返回它。否则,我们直接返回null。 lambda的最后一行表示返回值。所以这里将会返回一个CityForecast?类型的对象:
if (city != null) dataMapper.convertToDomain(city) else null DataMapper函数很简单: fun convertToDomain(forecast: CityForecast) = with(forecast) { val daily = dailyForecast.map { convertDayToDomain(it) } ForecastList(_id, city, country, daily) } private fun convertDayToDomain(dayForecast: DayForecast) = with(dayForecast) { Forecast(date, descript ion, high, low, iconUrl) }
最后完整的函数如下:
fun requestForecastByZipCode(zipCode: Long, date: Long) = forecastDbHelper.use { val dailyRequest = "${DayForecastTable.CITY_ID} = ? AND " + "${DayForecastTable.DATE} >= ?" val dailyForecast = select(DayForecastTable.NAME) .whereSimple(dailyRequest, zipCode.toString(), date.toString()) .parseList { DayForecast(HashMap(it)) } val city = select(CityForecastTable.NAME) .whereSimple("${CityForecastTable.ID} = ?", zipCode.toString()) .parseOpt { CityForecast(HashMap(it), dailyForecast) } if (city != null) dataMapper.convertToDomain(city) else null }
另一个Anko中好玩的功能我们在这里展示,就是可以使用classParser()来替代我们用的MapRowParser,它是基于列名通过反射的方式去生成对象的。 保存一个forecast saveForecast函数只是从数据库中清除数据,然后转换domain model为数据库model,然后插入每一天的forecast和city forecast。 这个结构比之前的更简单:它通过use函数从database helper中返回数据。在这个例子中我们不需要返回值,所以它将返回Unit。
fun saveForecast(forecast: ForecastList) = forecastDbHelper.use {
...
}
首先,我们清空这两个表。Anko没有提供比较漂亮的方式来做这个,但这并不意味着我们不行。 所以我们创建了一个SQLiteDatabase的扩展函数来让我们可以像SQL查询一样来执行它:
fun SQLiteDatabase.clear(tableName: String) { execSQL("delete from $tableName") }
清空这两个表:
clear(CityForecastTable.NAME)
clear(DayForecastTable.NAME)
现在,是时候去转换执行insert后返回的数据了。在这一点上你可能直到我是with函数的粉丝:
with(dataMapper.convertFromDomain(forecast)) {
...
}
从domain model转换的方式也是很直接的:
fun convertFromDomain(forecast: ForecastList) = with(forecast) { val daily = dailyForecast.map { convertDayFromDomain(id, it) } CityForecast(id, city, country, daily) } private fun convertDayFromDomain(cityId: Long, forecast: Forecast) =with(forecast) { DayForecast(date, descript ion, high, low, iconUrl, cityId) }
在代码块,我们可以在不使用引用和变量的情况下使用dailyForecast和map,只是像我们在这个类内部一样就可以了。 针对插入我们使用另外一个Anko函数,它需要一个表名和一个vararg修饰的Pair<String, Any>作为参数。 这个函数会把vararg转换成Android SDK需要的ContentValues对象。所以我们的任务组成是把map转换成一个vararg数组。 我们为MutableMap创建了一个扩展函数:
fun <K, V : Any> MutableMap<K, V?>.toVarargArray(): Array<out Pair<K, V>> = map({ Pair(it.key, it.value!!) }).toTypedArray()
它是支持可null的值的(这是map delegate的条件),把它转换为非null值(select函数需要)的Array所组成的Pairs。 不用担心就算你不完全理解这个函数,我很快就会讲到可空性。所以,这个新的函数我们可以这么使用:
insert(CityForecastTable.NAME, *map.toVarargArray())
它在CityForecast中插入了一个一行新的数据。在toVarargArray函数结果前面使用*表示这个array会被分解成为一个vararg参数。 这个在Java中是自动处理的,但是我们需要在Kotlin中明确指明。 每天的天气预报也是一样了:
dailyForecast.forEach { insert(DayForecastTable.NAME, *it.map.toVarargArray()) }
通过map的使用,我们可以用很简单的方式把类转换为数据表,反之亦然。 因为我们已经新建了扩展函数,我们可以在别的项目中使用,这个才是真正可贵的地方。 这个函数的完整代码如下:
fun saveForecast(forecast: ForecastList) = forecastDbHelper.use { clear(CityForecastTable.NAME) clear(DayForecastTable.NAME) with(dataMapper.convertFromDomain(forecast)) { insert(CityForecastTable.NAME, *map.toVarargArray()) dailyForecast forEach { insert(DayForecastTable.NAME, *it.map.toVarargArray()) } } }
博客学习来自《《Kotlin for Android Developers》中文翻译》。
相关代码可以查看: https://github.com/antoniolg/Kotlin-for-Android-Developers