7.1 数据持久化简介

数据持久化:是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机的情况下,这些数据仍然不会丢失。
保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的。持久化技术提供了一种机制,可以让数据在瞬时状态和持久状态之间进行转换。
Android系统中主要提供了3种方式用于简单地实现数据持久化功能:

  • 文件存储
  • SharedPreferences存储
  • 数据库存储

7.2 文件存储

基本概念

文件存储是Android中最基本的数据存储方式,它不对存储的内容进行任何格式化处理,所有数据都是原封不动地保存到文件当中的,因而它比较适合存储一些简单的文本数据或二进制数据。

数据存储到文件中

创建项目

首先创建一个FilePersistenceTest项目。

设置布局

并修改activity_main.xml中的代码,如下所示:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="vertical"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent" >
  5. <EditText
  6. android:id="@+id/editText"
  7. android:layout_width="match_parent"
  8. android:layout_height="wrap_content"
  9. android:hint="Type something here"/>
  10. </LinearLayout>

这里设立了一个输入框,在输入框中的输入的内容会短暂的进入内存当中进行存储,一旦按下”Back”键,输入的数据就会被回收,这就是瞬时数据

主要逻辑

在数据被回收之前,将它存储到文件当中。修改MainActivity中的代码,如下所示:

  1. class MainActivity : AppCompatActivity() {
  2. override fun onCreate(savedInstanceState: Bundle?) {
  3. super.onCreate(savedInstanceState)
  4. setContentView(R.layout.activity_main)
  5. }
  6. override fun onDestroy() {
  7. super.onDestroy()
  8. // 获取输入框的实例
  9. val editText:EditText = findViewById(R.id.editText)
  10. // 将输入框中输入的内容转换为字符串
  11. val inputText = editText.text.toString()
  12. // 保存字符串
  13. save(inputText)
  14. }
  15. private fun save(inputText: String) {
  16. try {
  17. val output = openFileOutput("data", Context.MODE_PRIVATE)
  18. val writer = BufferedWriter(OutputStreamWriter(output))
  19. writer.use {
  20. it.write(inputText)
  21. }
  22. } catch (e: IOException) {
  23. e.printStackTrace()
  24. }
  25. }
  26. }

在整个Activity销毁的时候,执行OnDestoryed方法,进行数据的存储,核心方法就是**save**,方法的参数就是需要保存的数据内容
我们把核心方法单独拉出来看一下:

  1. private fun save(inputText: String) {
  2. try {
  3. val output = openFileOutput("data", Context.MODE_PRIVATE)
  4. val writer = BufferedWriter(OutputStreamWriter(output))
  5. writer.use {
  6. // it指代的就是writer实例
  7. // write就是书写数据的函数
  8. it.write(inputText)
  9. }
  10. } catch (e: IOException) { // 抛出异常
  11. e.printStackTrace()
  12. }
  13. }

openFileOutput

首先是一个openFileOutput方法:

  • 第一参数用于指定文件名称,不能包含路径分隔符“/” ,如果文件不存在,Android 会自动创建它。创建的文件保存在/data/data/<package name>/files目录,如: /data/data/cn.itcast.action/files/data.txt
  • 第二参数用于指定操作模式,有四种模式,分别为:
    • Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容。
    • Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件。
    • MODE_WORLD_READABLE:表示当前文件可以被其他应用读取;
    • MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入。

接着是OutputStreamWriter,它是Writer子类,将输出的字符流变为字节流,即:将一个字符流的输出对象变为字节流的输出对象。
为什么要转换为字节流?
图片源自知乎

BufferedWriter

这里还有一个BufferedWriter方法,同样也是继承于WriterWriter可以理解为一个书写器,可以执行数据的书写的操作。而这个BufferedWriter拥有8192字符的缓冲区,使用BufferedWriter时,写入的数据并不会先输出到目的地,而是先存储至缓冲区中。如果缓冲区中的数据满了,才会一次对目的地进行写出。

use

最后这里使用了一个use函数,这是Kotlin提供的一个内置扩展函数。它保证在Lambda表达式中的代码全部执行完之后自动将外层的流关闭,这样就不需要再编写一个finally语句,手动去关闭流了,是一个非常好用的扩展函数。
也就是说在原始写入数据的时候,写入完毕需要关闭流(writer.close()),如果调用了use函数,就可以无需执行这个操作,它会自动关闭外层的流。

查看保存的数据

  1. 运行项目
  2. 打开Device File Explorer工具

image.png

  1. 打开/data/data目录

image.png

  1. 找到文件该项目文件(注意文件名称是com.example开头),打开file文件夹,就会发现data文件

image.png

文件中读取数据

和写入数据十分相似,所以这里就讲一下主要逻辑,首先是加载方法

  1. private fun load(): String {
  2. // 构建字符串构造器
  3. val content = StringBuilder()
  4. try {
  5. // 打开文件
  6. val input = openFileInput("data")
  7. // 使用字节流读取数据
  8. val reader = BufferedReader(InputStreamReader(input))
  9. reader.use {
  10. // 逐行读取内容,并把内容添加到content中
  11. reader.forEachLine {
  12. content.append(it)
  13. }
  14. }
  15. } catch (e: IOException) {
  16. e.printStackTrace()
  17. }
  18. return content.toString()
  19. }

这里最重要的就是读取数据,openFileInputInputStreamReaderBufferedReader的概念和之前讲的都是类似的,把写入改成读取即可。
完整实例:

  1. package com.example.filepersistencetest
  2. import android.content.Context
  3. import androidx.appcompat.app.AppCompatActivity
  4. import android.os.Bundle
  5. import android.widget.Button
  6. import android.widget.EditText
  7. import android.widget.Toast
  8. import com.example.database.MyDatabaseHelper
  9. import java.io.*
  10. class MainActivity : AppCompatActivity() {
  11. override fun onCreate(savedInstanceState: Bundle?) {
  12. super.onCreate(savedInstanceState)
  13. setContentView(R.layout.activity_main)
  14. // 调用加载方法,读取之前保存的数据
  15. val inputText = load()
  16. // 判断之前是否已经保存了数据
  17. if (inputText.isNotEmpty()) {
  18. val editText:EditText = findViewById(R.id.editText)
  19. // 设置输入框文本内容
  20. editText.setText(inputText)
  21. // 设置输入框光标的位置,在文本内容之后就只需要传入文本内容的长度即可
  22. editText.setSelection(inputText.length)
  23. // 保存的数据不为空就会弹出提示
  24. Toast.makeText(this, "Restoring succeeded", Toast.LENGTH_SHORT).show()
  25. }
  26. }
  27. private fun load(): String {
  28. val content = StringBuilder()
  29. try {
  30. val input = openFileInput("data")
  31. val reader = BufferedReader(InputStreamReader(input))
  32. reader.use {
  33. reader.forEachLine {
  34. content.append(it)
  35. }
  36. }
  37. } catch (e: IOException) {
  38. e.printStackTrace()
  39. }
  40. return content.toString()
  41. }
  42. override fun onDestroy() {
  43. super.onDestroy()
  44. val editText:EditText = findViewById(R.id.editText)
  45. val inputText = editText.text.toString()
  46. save(inputText)
  47. }
  48. private fun save(inputText: String) {
  49. try {
  50. val output = openFileOutput("data", Context.MODE_PRIVATE)
  51. val writer = BufferedWriter(OutputStreamWriter(output))
  52. writer.use {
  53. it.write(inputText)
  54. }
  55. } catch (e: IOException) {
  56. e.printStackTrace()
  57. }
  58. }
  59. }

本节参考

openFileOutput() - elleniou - 博客园
Java IO操作——字节-字符转换流(OutputStreamWriter、InputStreamReader)_LIUXUN1993728的博客-CSDN博客
吃透Java IO:字节流、字符流、缓冲流
Java中,BufferedReader类和BufferedWriter的介绍及作用_灵懈不同的施雅的博客-CSDN博客_bufferedwriter的作用


7.3 SharedPreferences存储

基本概念

不同于文件的存储方式,SharedPreferences是使用键值对的方式来存储数据的。
也就是说,当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。
而且SharedPreferences还支持多种不同的数据类型存储:

  • 如果存储的数据类型是整型,那么读取出来的数据也是整型的;
  • 如果存储的数据是一个字符串,那么读取出来的数据仍然是字符串

将数据存储到SharedPreferences中

类似于存储到文件当中一样,但是这里有两种方式:

  • Context类中的getSharedPreferences()方法,这个方法接受两个参数:
    • 第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在/data/data/<packagename>/shared_prefs/目录下的;
    • 第二个参数用于指定操作模式,目前只有默认的MODE_PRIVATE这一种模式可选,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。
  • Activity类中的getPreferences()方法
    • 这个方法和Context中的getSharedPreferences()方法很相似,不过它只接收一个操作模式参数,因为使用这个方法时会自动将当前Activity的类名作为SharedPreferences的文件名。

接下来是实践部分:

创建项目

创建一个SharedPreferencesTest项目

设置布局

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="match_parent"
  3. android:layout_height="match_parent"
  4. android:orientation="vertical" >
  5. <Button
  6. android:id="@+id/saveButton"
  7. android:layout_width="match_parent"
  8. android:layout_height="wrap_content"
  9. android:text="Save DaSharedPreferencesTestta"/>
  10. </LinearLayout>

主要逻辑

  1. package com.example.sharedpreferencestest
  2. import android.content.Context
  3. import androidx.appcompat.app.AppCompatActivity
  4. import android.os.Bundle
  5. import android.widget.Button
  6. class MainActivity : AppCompatActivity() {
  7. override fun onCreate(savedInstanceState: Bundle?) {
  8. super.onCreate(savedInstanceState)
  9. setContentView(R.layout.activity_main)
  10. val saveButton: Button = findViewById(R.id.saveButton)
  11. saveButton.setOnClickListener {
  12. // 获取SharedPreferences实例
  13. val editor = getSharedPreferences("data", Context.MODE_P
  14. RIVATE).edit()
  15. // 存储字符串数据
  16. editor.putString("name", "Tom")
  17. // 存储整形数据
  18. editor.putInt("age", 28)
  19. // 存储布尔类型数据
  20. editor.putBoolean("married", false)
  21. // 提交存储的数据
  22. editor.apply()
  23. }
  24. }
  25. }

非常的简单,getSharedPreferences的两个参数同上面的文件存储edit()用于开启编辑模式。其余的都写在注释里面的了,很好理解。

查看保存的数据

文件存储一样,在运行项目之后找到相应路径即可:
image.png
保存的文件是一个xml文件,内容很容易懂:

  1. <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
  2. <map>
  3. <string name="name">Tom</string>
  4. <boolean name="married" value="false" />
  5. <int name="age" value="28" />
  6. </map>

读取数据

SharedPreferences对象中提供了一系列的get方法,用于读取存储的 数 据,每 种get方法都对应了SharedPreferences.Editor中的一种put方法,比如读取一个布尔型数据就使用getBoolean()方法,读取一个字符串就使用getString()方法。这些get方法都接收两个参数

  • 第一个参数,传入存储数据时使用的键就可以得到相应的值了。
  • 第二个参数默认值,即表示当传入的键找不到对应的值时会以什么样的默认值进行返回。

修改布局

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="match_parent"
  3. android:layout_height="match_parent"
  4. android:orientation="vertical" >
  5. <Button
  6. android:id="@+id/saveButton"
  7. android:layout_width="match_parent"
  8. android:layout_height="wrap_content"
  9. android:text="Save Data"/>
  10. <Button
  11. android:id="@+id/restoreButton"
  12. android:layout_width="match_parent"
  13. android:layout_height="wrap_content"
  14. android:text="Restore Data"/>
  15. </LinearLayout>

添加读取逻辑

  1. val restoreButton:Button = findViewById(R.id.restoreButton)
  2. restoreButton.setOnClickListener {
  3. // 得到getSharedPreferences实例
  4. val prefs = getSharedPreferences("data", Context.MODE_PRIVATE)
  5. // 获取key是"name"的值,读取不到默认是空
  6. val name = prefs.getString("name", "")
  7. // 获取key是"age"的值,读取不到默认是0
  8. val age = prefs.getInt("age", 0)
  9. // 获取key是"married" 的值,读取不到默认是false
  10. val married = prefs.getBoolean("married", false)
  11. Log.d("MainActivity", "name is $name")
  12. Log.d("MainActivity", "age is $age")
  13. Log.d("MainActivity", "married is $married")
  14. }

逻辑也很简单,看注释即可。
完整实例:

  1. package com.example.sharedpreferencestest
  2. import android.content.Context
  3. import androidx.appcompat.app.AppCompatActivity
  4. import android.os.Bundle
  5. import android.util.Log
  6. import android.widget.Button
  7. class MainActivity : AppCompatActivity() {
  8. override fun onCreate(savedInstanceState: Bundle?) {
  9. super.onCreate(savedInstanceState)
  10. setContentView(R.layout.activity_main)
  11. val saveButton: Button = findViewById(R.id.saveButton)
  12. saveButton.setOnClickListener {
  13. val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
  14. editor.putString("name", "Tom")
  15. editor.putInt("age", 28)
  16. editor.putBoolean("married", false)
  17. editor.apply()
  18. }
  19. val restoreButton:Button = findViewById(R.id.restoreButton)
  20. restoreButton.setOnClickListener {
  21. val prefs = getSharedPreferences("data", Context.MODE_PRIVATE)
  22. val name = prefs.getString("name", "")
  23. val age = prefs.getInt("age", 0)
  24. val married = prefs.getBoolean("married", false)
  25. Log.d("MainActivity", "name is $name")
  26. Log.d("MainActivity", "age is $age")
  27. Log.d("MainActivity", "married is $married")
  28. }
  29. }
  30. }

image.png

实现记住密码功能

这里我们需要使用到前面写过的BroadCastBestPractice项目中的登录部分

布局文件修改

activity_login文件中的Button组件前添加如下布局:

  1. <LinearLayout
  2. android:orientation="horizontal"
  3. android:layout_width="match_parent"
  4. android:layout_height="wrap_content">
  5. <CheckBox
  6. android:id="@+id/rememberPass"
  7. android:layout_width="wrap_content"
  8. android:layout_height="wrap_content" />
  9. <TextView
  10. android:layout_width="wrap_content"
  11. android:layout_height="wrap_content"
  12. android:textSize="18sp"
  13. android:text="Remember password" />
  14. </LinearLayout>

也就是一个复选框,一个文本View,内容是Remember password

主要逻辑修改

  1. package com.example.broadcastbestpractice
  2. import android.content.Context
  3. import android.content.Intent
  4. import android.os.Bundle
  5. import android.widget.Button
  6. import android.widget.CheckBox
  7. import android.widget.EditText
  8. import android.widget.Toast
  9. class LoginActivity : BaseActivity() {
  10. override fun onCreate(savedInstanceState: Bundle?) {
  11. super.onCreate(savedInstanceState)
  12. setContentView(R.layout.activity_login)
  13. // 分别获取按钮,账户输入框,密码输入框,记得密码选项框的实例
  14. val login: Button = findViewById(R.id.login)
  15. val accountEdit:EditText = findViewById(R.id.accountEdit)
  16. val passwordEdit:EditText = findViewById(R.id.passwordEdit)
  17. val rememberPass: CheckBox = findViewById(R.id.rememberPass)
  18. // 下面是读取数据
  19. val prefs = getPreferences(Context.MODE_PRIVATE)
  20. // 获取存储的isRemember值,如果没有这个值,就初始化赋值为false
  21. val isRemember = prefs.getBoolean("remember_password", false)
  22. // 如果选项框在之前存储的值是ture就执行下面的代码,也就是说选项框之前被选中了才会执行
  23. if (isRemember) { // 将账号和密码都设置到文本框中
  24. // 获取存储的账户和密码的值,如果没有就是设置为""
  25. val account = prefs.getString("account", "")
  26. val password = prefs.getString("password", "")
  27. // 给两个输入框填入相应的值
  28. accountEdit.setText(account)
  29. passwordEdit.setText(password)
  30. rememberPass.isChecked = true
  31. }
  32. // 下面是登录事件,和之前一样
  33. login.setOnClickListener {
  34. val account = accountEdit.text.toString()
  35. val password = passwordEdit.text.toString()
  36. // 如果账号是admin且密码是123456,就认为登录成功
  37. if (account == "admin" && password == "123456") {
  38. // 从这里开始需要写入数据
  39. val editor = prefs.edit()
  40. // 检查复选框是否被选中
  41. if (rememberPass.isChecked) {
  42. editor.putBoolean("remember_password", true)
  43. editor.putString("account", account)
  44. editor.putString("password", password)
  45. } else {
  46. // 若未被选中,调用clear()方法,将SharedPreferences中的数据全部清除掉。
  47. editor.clear()
  48. }
  49. // 提交写入的数据
  50. editor.apply()
  51. val intent = Intent(this, MainActivity::class.java)
  52. startActivity(intent)
  53. finish()
  54. }
  55. else{
  56. Toast.makeText(this, "account or password is invalid",
  57. Toast.LENGTH_SHORT).show()
  58. }
  59. }
  60. }
  61. }

存储和读取数据部分逻辑如下:
第七章:数据持久化 - 图7

本节参考

Android中SharedPreferences.Editor的apply()与commit()方法BugFree张瑞的博客-CSDN博客_editor.apply();


7.4 SQLite数据库存储

Android系统内置了SQLite数据库SQLite是一款轻量级关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。
SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务,而SQLite又比一般的数据库要简单得多,它甚至不用设置用户名和密码就可以使用。
Android为了更加方便地管理数据库,专门提供了SQLiteOpenHelper帮助类,借助这个类可以非常简单地对数据库进行创建升级

基本使用

  1. SQLiteOpenHelper是一个抽象类,想要使用它,就需创建一个自己的帮助类去继承它。
  2. SQLiteOpenHelper中有两个抽象方法**onCreate()****onUpgrade()**。必须在自己的帮助类里重写这两个方法,然后分别在这两个方法中实现创建升级数据库的逻辑。
  3. SQLiteOpenHelper中还有两个非常重要的实例方法:**getReadableDatabase()****getWritableDatabase()**(一个是只读的方式打开,一个是可写入的方式打开)。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则要创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。
  4. SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少的那个构造方法即可。这个构造方法中接收4个参数:
    1. 第一个参数是Context,必须有它才能对数据库进行操作;
    2. 第二个参数是数据库名,创建数据库时使用的就是这里指定的名称;
    3. 第三个参数允许在查询数据时返回一个自定义的Cursor,一般传入null;
    4. 第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。

创建数据库

简单创建数据库

  1. 下载插件

image.png

  1. 添加数据库

image.png

  1. 选择SQLite

image.png

  1. 创建数据库

image.png

  1. 创建Book表

image.png

  1. 点击运行

image.png

  1. 刷新查看

image.png
image.png

如果刷新了仍然未能看到Book表,就重新Android Studio

代码中创建表

首先创建一个DatabaseTest项目,接下来创建一个MyDatabaseHelper继承于SQLiteOpenHelper

  1. package com.example.databasehelper
  2. import android.content.Context
  3. import android.database.sqlite.SQLiteDatabase
  4. import android.database.sqlite.SQLiteOpenHelper
  5. import android.widget.Toast
  6. class MyDatabaseHelper(val context: Context, name: String, version: Int) :
  7. SQLiteOpenHelper(context, name, null, version) {
  8. private val createBook = "create table Book (" +
  9. " id integer primary key autoincrement," +
  10. "author text," +
  11. "price real," +
  12. "pages integer," +
  13. "name text)"
  14. override fun onCreate(db: SQLiteDatabase) {
  15. db.execSQL(createBook)
  16. Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()
  17. }
  18. override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
  19. }
  20. }

这里的几个方法和参数的作用基本都在基本使用中讲过了,这里讲一下之前没讲过的。

  • createBook变量使用字符串的形式来存储sql语句。
  • execSQL用于执行sql语句,其中的参数就是一个字符串

接下来简单使用以下这个类:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="vertical"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <Button
  6. android:id="@+id/createDatabase"
  7. android:layout_width="match_parent"
  8. android:layout_height="wrap_content"
  9. android:text="Create Database"/>
  10. </LinearLayout>
  1. package com.example.databasetest
  2. import androidx.appcompat.app.AppCompatActivity
  3. import android.os.Bundle
  4. import android.widget.Button
  5. import com.example.databasehelper.MyDatabaseHelper
  6. class MainActivity : AppCompatActivity() {
  7. override fun onCreate(savedInstanceState: Bundle?) {
  8. super.onCreate(savedInstanceState)
  9. setContentView(R.layout.activity_main)
  10. val dbHelper = MyDatabaseHelper(this, "BookStore.db", 1)
  11. val createDatabase: Button = findViewById(R.id.createDatabase)
  12. createDatabase.setOnClickListener {
  13. dbHelper.writableDatabase
  14. }
  15. }
  16. }

调用逻辑十分的简单,就是构建一个帮助类,使用writableDatabase可读写的方式打开帮助类,这个时候帮助类调用启动方法OnCreated,执行了创建表的语句。
在老地方就可以找到创建的BookStore.db文件:
image.png
这个时候我们就可以把BookStore.db文件右击导出到自己新建的文件夹中进行使用。
image.png
接着就是在新建连接中,更改文件。
image.png

升级数据库

在上文中我们提到,**onUpgrade()**升级数据库的方法。

为什么要升级数据库

为什么需要升级?举个例子,我们在刚才的代码中添加一个Category表的创建:

  1. package com.example.databasehelper
  2. import android.content.Context
  3. import android.database.sqlite.SQLiteDatabase
  4. import android.database.sqlite.SQLiteOpenHelper
  5. import android.widget.Toast
  6. class MyDatabaseHelper(val context: Context, name: String, version: Int) :
  7. SQLiteOpenHelper(context, name, null, version) {
  8. private val createBook = "create table Book (" +
  9. " id integer primary key autoincrement," +
  10. "author text," +
  11. "price real," +
  12. "pages integer," +
  13. "name text)"
  14. private val createCategory = "create table Category (" +
  15. "id integer primary key autoincrement," +
  16. "category_name text," +
  17. "category_code integer)"
  18. override fun onCreate(db: SQLiteDatabase) {
  19. db.execSQL(createBook)
  20. db.execSQL(createCategory)
  21. Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()
  22. }
  23. override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
  24. }
  25. }

重新运行程序会发现,第二张Category表并没有被创建,其实是因为此时BookStore.db数据库已经存在,不管怎样点击“Create Database”按钮,MyDatabaseHelper中的onCreate()方法都不会再次执行,因此新添加的表也就无法创建。
最直白的解决方法就是将程序卸载重新安装。销毁数据库再重建,但是这样很麻烦。
所以有了升级方法,也可以理解为更新数据库

  1. override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
  2. db.execSQL("drop table if exists Book")
  3. db.execSQL("drop table if exists Category")
  4. onCreate(db)
  5. }

onUpgrade()方法中执行了两条**DROP语句**,若数据库中已经存在Book表或Category表,就将这两张表删除,然后调用onCreate()方法重新创建

如何升级数据库

方式很简单,SQLiteOpenHelper的构造方法里接收的第四个参数,表示当前数据库的**版本号**,之前传入的是1,现在只要传入一个比1大的数,就可以让onUpgrade()方法得到执行了。
把版本修改成为2之后,重新运行即可:
image.png

表的操作

这里PPT上面直接把四个方法写在onCreated方法里面,我觉得这样太拥挤了,所以单独拿出来写了四个初始化方法,最后在onCreated方法中调用即可。
最后我会把四个方法的完整实例代码放在一起展示。

注意我们这里每次操作之后都要重新导出db文件,才能查看操作之后的数据。

在布局中添加如下代码:

  1. <Button
  2. android:id="@+id/addData"
  3. android:layout_width="match_parent"
  4. android:layout_height="wrap_content"
  5. android:text="Add Data"
  6. />

增添数据的逻辑如下:

  1. private fun initAddDataFunction(){
  2. val addData:Button = findViewById(R.id.addData)
  3. addData.setOnClickListener {
  4. // 可写入的方式读取数据
  5. val db = dbHelper.writableDatabase
  6. val values1 = ContentValues().apply { // 开始组装第一条数据
  7. put("name", "The Da Vinci Code")
  8. put("author", "Dan Brown")
  9. put("pages", 454)
  10. put("price", 16.96)
  11. }
  12. db.insert("Book", null, values1) // 插入第一条数据
  13. val values2 = ContentValues().apply { // 开始组装第二条数据
  14. put("name", "The Lost Symbol")
  15. put("author", "Dan Brown")
  16. put("pages", 510)
  17. put("price", 19.95)
  18. }
  19. db.insert("Book", null, values2) // 插入第二条数据
  20. Toast.makeText(this,"insert successful",Toast.LENGTH_LONG).show()
  21. }
  22. }

这里我们通过ContentValues().apply{}来把不同的数据组装起来,形成一个元组进行插入,Book表一共有五个字段,所以一般要使用五个put对每个字段分别设置,这里的values2值插入了四个字段,是因为id作为主键,如果不插入就会自动增加

元组组装完成之后,就使用insert语句进行插入即可,这里的insert有三个参数:

  • 第一个参数是表名,希望向哪张表里添加数据,就传入该表的名字;
  • 第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般用不到这个功能,直接传入null即可;
  • 第三个参数是一个ContentValues对象,提供了一系列的put()方法重载,用于向ContentValues中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。

这一段使用sql就是:

  1. db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
  2. arrayOf("The Da Vinci Code", "Dan Brown", "454", "16.96")
  3. )
  4. db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
  5. arrayOf("The Lost Symbol", "Dan Brown", "510", "19.95")
  6. )

插入之后重新查询可得:image.png

增加一个删除用的按钮:

  1. <Button
  2. android:id="@+id/deleteData"
  3. android:layout_width="match_parent"
  4. android:layout_height="wrap_content"
  5. android:text="Delete Data"
  6. />

接下来是删除逻辑:

  1. private fun initDeleteFunction(){
  2. val deleteData: Button = findViewById(R.id.deleteData)
  3. deleteData.setOnClickListener {
  4. val db = dbHelper.writableDatabase
  5. db.delete("Book", "pages > ?", arrayOf("500"))
  6. }
  7. Toast.makeText(this,"delete successful",Toast.LENGTH_LONG).show()
  8. }

非常的简单,只需要一个delete函数即可,其中包含了三个参数:

  • 第一个参数仍然是表名;
  • 第二、第三个参数用于约束删除某一行或某几行的数据,不指定的话默认会删除所有行

第二个和第三个语句就类似于sql语句里面的where子句,只不过这里做了一个拆分,将约束的条件参数作为一个列表放在第三个参数的位置。
这一段使用sql就是:

  1. db.execSQL("delete from Book where pages > ?", arrayOf("500"))

添加修改用的按钮:

  1. <Button
  2. android:id="@+id/updateData"
  3. android:layout_width="match_parent"
  4. android:layout_height="wrap_content"
  5. android:text="Update Data"
  6. />

修改的核心逻辑:

  1. private fun initUpdateDate(){
  2. val updateData: Button = findViewById(R.id.updateData)
  3. updateData.setOnClickListener {
  4. val db = dbHelper.writableDatabase
  5. val values = ContentValues()
  6. values.put("price", 10.99)
  7. db.update("Book", values, "name = ?", arrayOf("The Da Vinci Code"))
  8. }
  9. Toast.makeText(this,"update successful",Toast.LENGTH_LONG).show()
  10. }

由于修改的只是一个字段,而不是元组,所以这里不需要进行组装。
根据sql里面的update语句,我们需要的就是一个字段+一个约束条件即可,所以update里面四个参数的就一目了然了。

  • 第一个:表名称
  • 第二个:修改的字段
  • 第三个、第四个:约束条件

上面这段代码就相当于:

  1. db.execSQL("update Book set price = ? where name = ?", arrayOf("10.99", "The Da Vinci Code"))

修改之后就是:
image.png

查询所需要用的方法是query(),参数如下:

参数 对应sql部分 描述
table from table_name 指定查询的表名
columns select column1,column2 指定查询的列名
selection where column = value 指定约束条件
selectionArgs - where占位符
groupBy group by column 指定需要分组的列
having having column = value 对group by进行约束
orderBy order by column1,column2 指定排序方式

添加一个查询按钮:

  1. <Button
  2. android:id="@+id/queryData"
  3. android:layout_width="match_parent"
  4. android:layout_height="wrap_content"
  5. android:text="Query Data"
  6. />

查询的核心逻辑:

  1. private fun initQueryData(){
  2. val queryData: Button = findViewById(R.id.queryData)
  3. queryData.setOnClickListener {
  4. val db = dbHelper.writableDatabase // 查询Book表中所有的数据
  5. val cursor = db.query("Book", null, null, null, null, null, null)
  6. if (cursor.moveToFirst()) {
  7. do { // 遍历Cursor对象,取出数据并打印
  8. val name = cursor.getString(cursor.getColumnIndexOrThrow("name"))
  9. val author = cursor.getString(cursor.getColumnIndexOrThrow("author"))
  10. val pages = cursor.getInt(cursor. getColumnIndexOrThrow("pages"))
  11. val price = cursor.getDouble(cursor. getColumnIndexOrThrow("price"))
  12. Log.d("MainActivity", "book name is $name")
  13. Log.d("MainActivity", "book author is $author")
  14. Log.d("MainActivity", "book pages is $pages")
  15. Log.d("MainActivity", "book price is $price")
  16. } while (cursor.moveToNext())
  17. }
  18. cursor.close()
  19. }
  20. Toast.makeText(this,"query successful",Toast.LENGTH_LONG).show()
  21. }

这里的查询语句非常简单:db.query("Book", null, null, null, null, null, null),说明查询全表,主要是遍历cursor对象输出查询的结果有点多,实际也不难。

  • moveToFirst():用于获取查询结果的第一个元组,如果这个数据存在,说明查询不为空,使用do while循环进行遍历即可。
  • moveToNext():获取下一个元组,如果下一个元组不存在,说明已经遍历完成了。
  • getColumnIndexOrThrow:用于通过索引获取相应的值,其实也可以使用getColumnIndex,但是有可能这个所以不存在,所以使用getColumnIndexOrThrow
  • getDataType:把获取到的数据转换为DataType类型,比如StringInt
  • cursor.close():最后注意关闭cursor

查询语句相当于:

  1. val cursor = db.rawQuery("select * from Book", null)

完整实例代码

点击三角符号可以查看。

  1. package com.example.databasetest
  2. import android.content.ContentValues
  3. import androidx.appcompat.app.AppCompatActivity
  4. import android.os.Bundle
  5. import android.util.Log
  6. import android.widget.Button
  7. import android.widget.Toast
  8. import com.example.databasehelper.MyDatabaseHelper
  9. class MainActivity : AppCompatActivity() {
  10. private val dbHelper = MyDatabaseHelper(this, "BookStore.db", 3)
  11. override fun onCreate(savedInstanceState: Bundle?) {
  12. super.onCreate(savedInstanceState)
  13. setContentView(R.layout.activity_main)
  14. val createDatabase: Button = findViewById(R.id.createDatabase)
  15. createDatabase.setOnClickListener {
  16. dbHelper.writableDatabase
  17. }
  18. // 增添数据
  19. initAddDataFunction()
  20. // 删除数据
  21. initDeleteFunction()
  22. // 修改数据
  23. initUpdateDate()
  24. // 查询数据
  25. initQueryData()
  26. }
  27. private fun initAddDataFunction(){
  28. val addData:Button = findViewById(R.id.addData)
  29. addData.setOnClickListener {
  30. val db = dbHelper.writableDatabase
  31. val values1 = ContentValues().apply { // 开始组装第一条数据
  32. put("name", "The Da Vinci Code")
  33. put("author", "Dan Brown")
  34. put("pages", 454)
  35. put("price", 16.96)
  36. }
  37. db.insert("Book", null, values1) // 插入第一条数据
  38. val values2 = ContentValues().apply { // 开始组装第二条数据
  39. put("name", "The Lost Symbol")
  40. put("author", "Dan Brown")
  41. put("pages", 510)
  42. put("price", 19.95)
  43. }
  44. db.insert("Book", null, values2) // 插入第二条数据
  45. Toast.makeText(this,"insert successful",Toast.LENGTH_LONG).show()
  46. }
  47. }
  48. private fun initDeleteFunction(){
  49. val deleteData: Button = findViewById(R.id.deleteData)
  50. deleteData.setOnClickListener {
  51. val db = dbHelper.writableDatabase
  52. db.delete("Book", "pages > ?", arrayOf("500"))
  53. }
  54. Toast.makeText(this,"delete successful",Toast.LENGTH_LONG).show()
  55. }
  56. private fun initUpdateDate(){
  57. val updateData: Button = findViewById(R.id.updateData)
  58. updateData.setOnClickListener {
  59. val db = dbHelper.writableDatabase
  60. val values = ContentValues()
  61. values.put("price", 10.99)
  62. db.update("Book", values, "name = ?", arrayOf("The Da Vinci Code"))
  63. }
  64. Toast.makeText(this,"update successful",Toast.LENGTH_LONG).show()
  65. }
  66. private fun initQueryData(){
  67. val queryData: Button = findViewById(R.id.queryData)
  68. queryData.setOnClickListener {
  69. val db = dbHelper.writableDatabase // 查询Book表中所有的数据
  70. val cursor = db.query("Book", null, null, null, null, null, null)
  71. if (cursor.moveToFirst()) {
  72. do { // 遍历Cursor对象,取出数据并打印
  73. val name = cursor.getString(cursor.getColumnIndexOrThrow("name"))
  74. val author = cursor.getString(cursor.getColumnIndexOrThrow("author"))
  75. val pages = cursor.getInt(cursor. getColumnIndexOrThrow("pages"))
  76. val price = cursor.getDouble(cursor. getColumnIndexOrThrow("price"))
  77. Log.d("MainActivity", "book name is $name")
  78. Log.d("MainActivity", "book author is $author")
  79. Log.d("MainActivity", "book pages is $pages")
  80. Log.d("MainActivity", "book price is $price")
  81. } while (cursor.moveToNext())
  82. }
  83. cursor.close()
  84. }
  85. Toast.makeText(this,"query successful",Toast.LENGTH_LONG).show()
  86. }
  87. }