Kotlin开发Android笔记13:Android数据库(SQLite)操作
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 DESCRIPTION = "description"
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() }
简易安卓app开发}
instance这个属性使⽤了lazy委托,表⽰直到它真的被调⽤才会被创建。⽤这种⽅法,如果数据库从来没有被使⽤,没有必要去创建这个对象。
⼀般lazy委托的代码块可以阻⽌在多个不同的线程中创建多个对象。这个只会发⽣在两个线程在同事时间访问这个instance对象,
它很难发⽣但是发⽣具体还有看app的实现。⽆⼈如何,lazy委托是线程安全的。
创建这些定义的表,需要去提供⼀个onCreate函数的实现。当没有库使⽤的时候,创建表会通过写原⽣的包含定义好的列和类型的CREATE TABLE语句来实现。
然⽽Anko提供了⼀个简单的扩展函数,它接收⼀个表的名字和⼀系列由列名和类型构建的Pair对象:
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
然后,把他们应⽤到表的创建中:
CityForecastTable.ID to INTEGER + PRIMARY_KEY,
CityForecastTable.CITY to TEXT,
CityForecastTable.COUNTRY to TEXT)
这就是整个函数看起来的样⼦:
override fun onCreate(db: SQLiteDatabase) {
CityForecastTable.ID to INTEGER + PRIMARY_KEY,
CityForecastTable.CITY to TEXT,
CityForecastTable.COUNTRY to TEXT)
DayForecastTable.ID to INTEGER + PRIMARY_KEY + AUTOINCREMENT,
DayForecastTable.DATE to INTEGER,
DayForecastTable.DESCRIPTION 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
}
}
默认的构造函数会得到⼀个含有属性和对应的值的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 description: 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, description: String, high: Int, low: Int,
iconUrl: String, cityId: Long)
: this(HashMap()) {
this.date = date
this.description = description
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, String(), String())
.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。
下⼀步查询城市也是⼀样的⽅法:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论