Android 数据存储三种方式(一)https://blog.youkuaiyun.com/Tefuir111/article/details/123867585
三、SQLite数据库存储
这是一个重要的技术,SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。
SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务,而且SQLite比一般的数据库要简单得多,它甚至不用设置用户名和密码就可以使用。Android正是把这个功能极为强大的数据库嵌入到了系统当中,使得本地持久化的功能有了一次质的飞跃。
1、创建数据库
Android为了方便管理SQLite,提供了一个SQLiteOpenHelper帮助类。
SQLiteOpenHelper帮助类是一个抽象类。所以在我们使用它时需要创建一个自己的帮助类去实现它。SQLiteOpenHelper 有两个抽象方法 onCreate() 和 onUpgrade()。我们必须在自己的帮助类中重写这两个方法。
SQLiteOpenHelper中还有两个非常重要的实例方法: 和
。
这两个方法都可以创建或打开一个现有的数据库(如果一个数据库存在则直接打开,否则新建一个新的数据库),并返回一个可以对数据库进行读写操作的对象。
不同的是:当数据库不可写入(如磁盘空间满时),返回的对象是将以只读方式打开数据库,
会抛出一个异常。
SQLiteOpenHelper 有两个构造函数可以重写,一般使用参数少的那个即可。该构造函数有四个参数:
第一个参数为Context;
第二个参数为数据库名;
第三个参数允许我们在查询数据时返回一个自定义的Cursor,一般传入null;
第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。
创建数据库的示例代码如下:
class MyDatabaseHelper(private val context: Context, name: String, version: Int) :
SQLiteOpenHelper(context, name, null, version) {
private val createBook = "create table Book (" +
" id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)
db.execSQL(createCategory)
Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()
}
}
构建出SQLiteOpenHelper实例之后,调用或者
即可创建出数据库,存放在 /data/data/<package name>/databases/ 目录下。此时重写的
函数会被执行,通常会在此处理一些创建表的逻辑。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 1)
createDatabase.setOnClickListener {
dbHelper.writableDatabase
}
}
}
在onCreate()方法中构建了一个MyDatabaseHelper对象,并且通过构造函数的参数将数据库名指定为BookStore.db,版本号指定为1,然后在“Create Database”按钮的点击事件里调用了getWritableDatabase()方法。
2、升级数据库
private val createCategory = "create table Category (" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("drop table if exists Book")
db.execSQL("drop table if exists Category")
onCreate(db)
}
在onUpgrade()方法中执行了两条DROP语句,如果发现数据库中已经存在Book表或Category表,就将这两张表删除,然后调用onCreate()方法重新创建。
这里先将已经存在的表删除,是因为如果在创建表时发现这张表已经存在了,就会直接报错。
这里需要修改MainActivity中:
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
version变成2,才能重新导入。
学习一下如何对表中的数据进行操作了。其实我们可以对数据进行的操作无非有4种,即CRUD。其中C代表添加(create),R代表查询(retrieve),U代表更新(update),D代表删除(delete)
前面我们已经知道,调用SQLiteOpenHelper的getReadableDatabase()或getWritableDatabase()方法是可以用于创建和升级数据库的,不仅如此,这两个方法还都会返回一个SQLiteDatabase对象,借助这个对象就可以对数据进行CRUD操作了。
3、添加数据
SQLiteDatabase中提供了一个insert()方法,专门用于添加数据。它接收3个参数:
第一个参数是表名,表示希望向哪张表添加数据;
第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般用不到,直接传入null即可。
第三个参数是一个ContentValues对象,提供了一系列的put()方法重载,用于向ContentValues添加数据。只需要将表中的每个列名以及相应的待添加数据传入即可。
示例代码如下:
addData.setOnClickListener {
val db = dbHelper.writableDatabase
val value1 = ContentValues().apply {
put("name", "The Da Vinci Code")
put("author", "Dan Brown")
put("pages", 454)
put("price", 10.08)
}
db.insert("Book", null, value1)
val value2 = ContentValues().apply {
put("name", "Nuo wei's Forest")
put("author", "Cun Shang Chun Shu")
put("pages", 520)
put("price", 6.23)
}
db.insert("Book", null, value2)
}
先建立一个ContentValues()对象,然后通过put()将表中的每个列名以及相应的待添加数据传入即可。
扩展知识:
虽然可以使用扩展函数简化代码,但是可以进一步简化,类似于键值对的方式。重新构建一个cvOf()方法,如下:
fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {
val cv = ContentValues()
for (pair in pairs) {
val key = pair.first
when (val value = pair.second) {
is Int -> cv.put(key, value)
is Double -> cv.put(key, value)
is Float -> cv.put(key, value)
is Long -> cv.put(key, value)
is Short -> cv.put(key, value)
is Boolean -> cv.put(key, value)
is Byte -> cv.put(key, value)
is ByteArray -> cv.put(key, value)
is String -> cv.put(key, value)
null -> cv.putNull(key)
}
}
return cv
}
vararg 关键字代表:我们允许向这个方法传入0个、1个、2个甚至任意多个Pair类型的参数,然后可通过 for...in 循环将参数遍历出来。
我们使用for-in循环遍历了pairs参数列表,在循环中取出了key和value,并使用when语句来判断value的类型。注意,这里将ContentValues所支持的所有数据类型全部覆盖了进去,然后将参数中传入的键值对逐个添加到ContentValues中,最终将ContentValues返回。
使用时代码:
val values3 = cvOf(
"name" to "Dou Luo da Lu",
"author" to "Mr.Tang",
"price" to 22.2,
"pages" to 12341
)
db.insert("Book", null, values3)
其实Google提供的KTX扩展库中已经包含了上述ContentValues的简化用法,这个扩展库会在Android Studio创建项目的时候自动引入build.gradle的dependencies中
它的函数是 contentValuesOf() 。具有和上述相同效果!!!
4、更新数据
SQLiteDatabase中提供了一个非常好用的update()方法,用于对数据进行更新。这个方法接收4个参数:
第一个参数和insert()方法一样,也是表名,指定更新哪张表里的数据;
第二个参数是ContentValues对象,要把更新数据在这里组装进去;
第三、第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认会更新所有行。
示例代码如下:
updateData.setOnClickListener {
val db = dbHelper.writableDatabase
val values = ContentValues()
values.put("price", 1.24)
db.update("Book", values, "name = ?", arrayOf("The Da Vinci Code"))
}
5、删除数据
SQLiteDatabase中提供了一个delete()方法,专门用于删除数据。
这个方法接收3个参数:
第一个参数仍然是表名;
第二、第三个参数用于约束删除某一行或某几行的数据,不指定的话默认会删除所有行。
示例代码:
deleteData.setOnClickListener {
val db = dbHelper.writableDatabase
db.delete("Book", "pages > ?", arrayOf("300"))
}
6、查询数据
SQLiteDatabase中还提供了一个query()方法用于对数据进行查询。
这个方法的参数很复杂,最短的一个方法重载也需要7个参数:
第一个参数:表名
第二个参数:指定查询那几列,如果不指定则默认查询所有列
第三、四个参数:用于约束查询某一行或某几行的数据,不指定则默认所有行
第五个参数:指定需要去group by的列,不指定则表示不对查询结果进行group by操作。
第六个参数:用于对group by之后的数据进行进一步的过滤,不指定则不进行过滤。
第七个参数:用于指定查询结果的排序方式,不指定则使用默认的排序方式。
示例代码:
queryData.setOnClickListener {
val db = dbHelper.writableDatabase
val cursor = db.query("Book", null, null, null, null, null, null)
if (cursor.moveToFirst()) {
do {
val name = cursor.getString(cursor.getColumnIndex("name"))
val pages = cursor.getString(cursor.getColumnIndex("pages"))
val author = cursor.getString(cursor.getColumnIndex("author"))
val price = cursor.getString(cursor.getColumnIndex("price"))
Log.d("MainActivity", "book name is $name")
Log.d("MainActivity", "book author is $author")
Log.d("MainActivity", "book pages is $pages")
Log.d("MainActivity", "book price is $price")
} while (cursor.moveToNext())
}
cursor.close()
}
query() 函数只赋值第一个参数为表名,表示查询所有数据,返回一个Cursor对象,调用moveToFirst() 方法,移动到第一行,然后做循环直到下一行为空!
循环中通过Cursor对象的 getColumnIndex() 方法获取某一列在表中对应的位置索引。
可通过Log来查看有没有读取成功。
最后别忘了调用 close() 方法来关闭Cursor!!!!!
7、使用事务
SQLite数据库是支持事务的,事务的特性可以保证让一系列的操作要么全部完成,要么一个都不会完成。
启动事务的示例代码:(删掉Book中所有的旧数据,添加新数据)
replaceData.setOnClickListener {
val db = dbHelper.writableDatabase
db.beginTransaction()
try {
db.delete("Book", null, null)
if (true) {
throw NullPointerException()
}
val values = ContentValues().apply {
put("name", "Game of Thrones")
put("author", "George Martin")
put("pages", 720)
put("price", 20.85)
}
db.insert("Book", null, values)
db.setTransactionSuccessful() //表示事务已经执行成功
} catch (e: Exception) {
e.printStackTrace()
} finally {
db.endTransaction() //结束掉事务
}
}
上述代码就是Android中事务的标准用法!!!!
首先调用 beginTransaction() 方法开启一个事务,然后在一个异常捕获代码块中执行具体的数据库操作,一但代码块中抛出异常,就会结束掉整个事务,抛出异常之前做的操作会全部返回,数据库会回到事务开始之前的状态!!
反映在程序中的情况是:点击replaceData按钮之后,在点击queryData按钮查询到的数据仍旧是旧数据。说明事务生效了,否则delete()的操作会执行!!!!
注释掉抛出异常的代码行,事务能顺利执行,点击replaceData按钮之后,在点击queryData按钮查询到的数据是新添加的数据!!!!
当操作完成后,调用 setTransactionSuccessful() 表示事务已经执行成功,最后在finally代码块中调用 endTransaction() 结束事务。
8、升级数据库的最佳写法
之前升级数据库,直接使用 drop 语句将之前的数据库都抛弃掉,然后重新调用onCreate(db)来重新创建数据库,这样太简单暴力了,会导致一个问题:之前的用户保存的数据在升级后,会随着更新都消失,相当于之前的数据进行了初始化,用户体验极差。
下面我们就来学习一下如何实现这样的功能。每一个数据库版本都会对应一个版本号,当指定的数据库版本号大于当前数据库版本号的时候,就会进入onUpgrade()方法中执行更新操作。这里需要为每一个版本号赋予其所对应的数据库变动,然后在onUpgrade()方法中对当前数据库的版本号进行判断,再执行相应的改变就可以了。
修改之前的代码,在版本2新建了一个Category表,修改 MyDatabaseHelper 中的代码如下:
private val createCategory = "create table Category (" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)
db.execSQL(createCategory)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion <= 1) {
db.execSQL(createCategory)
}
}
在onUpgrade()方法中添加了一个if判断,如果用户数据库的旧版本号小于等于1,就只会创建一张Category表。
又来一个新的需求,需要建立Book表和Category表的连接,为Book表添加一个新的category_id字段:
修改代码如下:
private val createBook = "create table Book (" +
" id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text," +
"category_id integer)"
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion <= 1) {
db.execSQL(createCategory)
}
if (oldVersion <= 2) {
db.execSQL("alter table Book add column category_id integer")
}
}
createBook 添加了category_id字段,在onUpgrade() 中,添加一个新的条件,如果当前数据库的版本号是2,就会执行alter命令,为Book表新增一个category_id列。
重要的细节!!!!!:每一个版本都要对应一个if判断语句,这是为了保证程序在跨版本升级的时候能够将跨过版本更新的每一版所要作的修改都能进行更新,保证数据库的结构是最新的。
execSQL() 方法内部能够直接调用SQL语句!!!!
之前的增删改查CRUD操作可以写成:
添加数据:
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
arrayOf("The Da Vinci Code", "Dan Brown", "454", "16.96")
)
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
arrayOf("The Lost Symbol", "Dan Brown", "510", "19.95")
)
更新数据:
db.execSQL("update Book set price = ? where name = ?", arrayOf("10.99", "The Da Vinci Code"))
删除数据:
db.execSQL("delete from Book where pages > ?", arrayOf("500"))
查询数据:
val cursor = db.rawQuery("select * from Book", null)
根据个人喜好!!!!!