抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

课本

书籍资源进入官网下载,PC端进入

第七章-数据存储全方案,详解持久化技术

引言

任何一个应用程序,其实说白了就是在不停地和数据打交道,我们聊QQ、看新闻、刷微博,所关心的都是里面的数据,没有数据的应用程序就变成了一个空壳子,对用户来说没有任何实际用途。那么这些数据是从哪儿来的呢?现在多数的数据基本是由用户产生的,比如你发微博、评论新闻,其实都是在产生数据。
我们前面章节所编写的众多例子中也使用到了一些数据,例如第4章最佳实践部分在聊天界面编写的聊天内容,第6章最佳实践部分在登录界面输入的账号和密码。这些数据有一个共同点,即它们都属于瞬时数据。那么什么是瞬时数据呢?就是指那些存储在内存当中,有可能会因为程序关闭或其他原因导致内存被回收而丢失的数据。这对于一些关键性的数据信息来说是绝对不能容忍的,谁都不希望自己刚发出去的一条微博,刷新一下就没了吧。那么怎样才能保证一些关键性的数据不会丢失呢?这就需要用到数据持久化技术了。

简介

数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的。持久化技术提供了一种机制,可以让数据在瞬时状态和持久状态之间进行转换
持久化技术被广泛应用于各种程序设计领域,而本节要探讨的自然是Android中的数据持久化技术。Android系统中主要提供了3种方式用于简单地实现数据持久化功能:文件存储SharedPreferences存储以及数据库存储
下面我就将对这3种数据持久化的方式一一进行详细的讲解。

文件存储

文件存储是Android中最基本的数据存储方式,它不对存储的内容进行任何格式化处理,所有数据都是原封不动地保存到文件当中的,因而它比较适合存储一些简单的文本数据或二进制数据。如果你想使用文件存储的方式来保存一些较为复杂的结构化数据,就需要定义一套自己的格式规范,方便之后将数据从文件中重新解析出来。

将数据存储到文件中

Context类提供了openFileOutput()方法可以将数据存到指定文件中.该方法接受两个参数:

  • 参数1: 文件名,文件名不可以不可以包含路径,因为所有文件默认保存到到/data/data/<package name>/files/目录
  • 参数2: 文件操作模式,有MODE_PRIVATE(默认)覆盖已有内容写入和MODE_APPEND追加写入内容可选.(MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE已在Android4.2被删除)

下面我们就编写一个完整的例子,借此学习一下如何在Android项目中使用文件存储的技术。

  1. 先创建一个FilePersistenceTest项目,并修改activity_main.xml中的代码,如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent" >
     <EditText
         android:id="@+id/editText"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:hint="Type something here"
         />
    </LinearLayout>
    
    这里只是在布局中加入了一个EditText,用于输入文本内容。
    现在运行程序,界面上只有一个输入框.在输入框内输入的东西按下Back键时数据就会丢失是因为当Activity被销毁后就会被回收.而我们要做的下一步就是在数据回收前把数据保存到文件中.
  2. 修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
     }
    
     override fun onDestroy() {
         super.onDestroy()
         val inputText = findViewById<EditText>(R.id.editText).text.toString()
         val output = openFileOutput("data",Context.MODE_PRIVATE)
         val writer = BufferedWriter(OutputStreamWriter(output))
         writer.use {
             it.write(inputText)
         }
     }
    }
    

    我们重写了onDestroy()方法,这样就可以保证在Activity销毁之前一定会调用这个方法。在onDestroy()方法中,我们获取了EditText中输入的内容,将内容保存到名为data的文件中.

  3. 重新运行一下程序,并在EditText中输入一些内容,如图所示。
    博客第一行代码3学习文件持久化1
  4. 这时候按下Back,输入的内容就保存到文件中了,然后打开Device File Explorer查看文件(如果没有的使
    用快捷键Ctrl + Shift + A搜索Device File Explorer).
  5. 我们在这里找到/data/data/com.example.filepersistencetest/files/目录,可以看到,现在已经生成了一个data文件,如图所示。
    博客第一行代码3学习文件持久化2
    这样就证实了在EditText中输入的内容确实已经成功保存到文件中了。
    不过,只是成功将数据保存下来还不够,我们还需要想办法在下次启动程序的时候让这些数据能够还原到EditText中,因此接下来我们就要学习一下如何从文件中读取数据。

从文件中读取数据

类似于将数据存储到文件中,Context类中还提供了一个openFileInput()方法,用于从文件中读取数据。这个方法要比openFileOutput()简单一些,它只接收一个参数,即要读取的文件名,然后系统会自动到/data/data/<package name>/files/目录下加载这个文件,并返回一个FileInputStream对象,得到这个对象之后,再通过流的方式就可以将数据读取出来了。

  1. 以下是一段简单的代码示例,展示了如何从文件中读取文本数据:

    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val content = StringBuilder()
         val input = openFileInput("data")
         val reader = BufferedReader(InputStreamReader(input))
         reader.use {
             reader.forEachLine {
                 content.append(it)
             }
         }
         Toast.makeText(this, "读取到缓存文件内容:$content", Toast.LENGTH_SHORT).show()
     }
    
     override fun onDestroy() {
         super.onDestroy()
         val inputText = findViewById<EditText>(R.id.editText).text.toString()
         val output = openFileOutput("data",Context.MODE_PRIVATE)
         val writer = BufferedWriter(OutputStreamWriter(output))
         writer.use {
             it.write(inputText)
         }
     }
    }
    

    在这段代码中,首先通过openFileInput()方法获取了一个FileInputStream对象,然后借助它又构建出了一个InputStreamReader对象,接着再使用InputStreamReader构建出一个BufferedReader对象,这样我们就可以通过BufferedReader将文件中的数据一行行取出来,并拼接到StringBuilder对象当中。注意,这里从文件中读取数据使用了一个forEachLine函数,这也是Kotlin提供的一个内置扩展函数,它会将读到的每内容都回调到Lambda表达式中,我们在Lambda表达式中完成拼接逻辑即可。
    重新启动程序时EditText中能够保留我们上次输入的内容
    博客第一行代码3学习文件持久化3

Github代码

SharedPreferences存储

概述

SharedPreferences是使用键值对的方式进行存储数据的,存储的时候是什么类型的数据,取出来就是什么类型的数据.

两种存储方法

要想使用SharedPreferences存储数据,首先需要获取SharedPreferences对象。Android中主要提供了以下两种方法用于得到SharedPreferences对象。

Context类中的getSharedPreferences()方法

此方法接收两个参数:

  • 第一个参数: 文件名称,文件不存在则创建文件,文件存放在在/data/data//shared_prefs/目录下
  • 第二个参数: 指定模式,只有MODE_PRIVATE可选,和传入0是相同的.表示只有当前应用程序才能读写,其它操作模式已被废弃.

    Activity类中的getPreferences()方法

    和Context中的getSharedPreferences()方法很相似,不过它只接收一个操作模式参数,因为使用这个方法时会自动将当前Activity的类名作为SharedPreferences的文件名

如何使用SharedPreferences

得到了SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了,主要可以分为3步实现。

  1. 调用SharedPreferences对象的edit()方法获取一个SharedPreferences.Editor对象。
  2. SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用putBoolean()方法,添加一个字符串则使用putString()方法,以此类推
  3. 调用apply()方法将添加的数据提交,从而完成数据存储操作。

使用SharedPreferences写入数据

  1. 新建一个SharedPreferencesTest项目,然后修改activity_main.xml中的代码,如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
     <Button
         android:id="@+id/saveButton"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="保存数据"/>
    </LinearLayout>
    
    在这里我们只是放置了一个按钮
  2. 然后修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
    
         val saveButton = findViewById<Button>(R.id.saveButton)
         saveButton.setOnClickListener {
             // 获得一个SharedPreferences对象
             val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
             editor.putString("姓名","小明")
             editor.putInt("年龄",18)
             editor.putBoolean("婚配",false)
             // 保存数据
             editor.apply()
         }
     }
    }
    

    这里首先给按钮注册了一个点击事件,然后在点击事件中通过getSharedPreferences()方法指定SharedPreferences的文件名为data,并得到了SharedPreferences.Editor对象。接着向这个对象中添加了3条不同类型的数据,最后调用apply()方法进行提交,从而完成了数据存储的操作。

  3. 运行程序,点击保存数据后打开/data/data/com.example.sharedpreferencestest/shared_prefs/目录下查看文件.
    博客第一行代码sharedpreferencestest运行效果图1
    可以看到,我们刚刚在按钮的点击事件中添加的所有数据都已经成功保存下来了,并且SharedPreferences文件是使用XML格式来对数据进行管理的。

    使用SharedPreferences读取数据

    读取数据相较于写入数据会更加简单,SharedPreferences对象中提供了一系列的get方法,用于读取存储的数据,每种get方法都对应了SharedPreferences.Editor中的一种put方法,比如读取一个布尔型数据就使用getBoolean()方法,读取一个字符串就使用getString()方法。这些get方法都接收两个参数:第一个参数是,传入存储数据时使用的键就可以得到相应的值了;第二个参数是默认值,即表示当传入的键找不到对应的值时会以什么样的默认值进行返回。
  4. 是在SharedPreferencesTest项目的基础上继续开发,修改activity_main.xml中的代码,如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
     <Button
         android:id="@+id/saveButton"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="保存数据"/>
     <Button
         android:id="@+id/restoreButton"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="还原数据"/>
    </LinearLayout>
    
    这里增加了一个还原数据的按钮,我们希望通过点击这个按钮来从SharedPreferences文件中读取数据。
  5. 修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val saveButton = findViewById<Button>(R.id.saveButton)
         saveButton.setOnClickListener {
             val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
             editor.putString("姓名","小明")
             editor.putInt("年龄",18)
             editor.putBoolean("婚配",false)
             editor.apply()
         }
    
         val restoreButton = findViewById<Button>(R.id.restoreButton)
         restoreButton.setOnClickListener {
             // 获得SharedPreferences对象
             val prefs = getSharedPreferences("data",Context.MODE_PRIVATE)
             // 获得文件内容
             val name = prefs.getString("姓名","")
             val age = prefs.getInt("年龄",0)
             val married = prefs.getBoolean("婚配",false)
             Log.d("MainActivity","姓名是$name")
             Log.d("MainActivity","年龄是$age")
             Log.d("MainActivity","婚配是$married")
         }
     }
    }
    

    可以看到,首先通过getSharedPreferences()方法得到了SharedPreferences对象,然后分别调用它的getString()getInt()getBoolean()方法,去获取前面所存储的姓名、年龄和是否已婚,如果没有找到相应的值,就会使用方法中传入的默认值来代替,最后通过Log将这些值打印出来。
    博客第一行代码3学习SharedPreferences日志输出1

实现记住密码功能

既然是实现记住密码的功能,那么我们就不需要从头去写了,因为在上一章中的最佳实践部分已经编写过一个登录界面了,有可以重用的代码为什么不用呢?那就首先打开BroadcastBestPractice项目,编辑一下登录界面的布局。

  1. 修改activity_login.xml中的代码,如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
     <LinearLayout
         android:orientation="horizontal"
         android:layout_width="match_parent"
         android:layout_height="60dp">
         <TextView
             android:layout_width="90dp"
             android:layout_height="wrap_content"
             android:layout_gravity="center_vertical"
             android:textSize="18sp"
             android:text="Account:" />
         <EditText
             android:id="@+id/accountEdit"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
             android:layout_gravity="center_vertical" />
     </LinearLayout>
     <LinearLayout
         android:orientation="horizontal"
         android:layout_width="match_parent"
         android:layout_height="60dp">
         <TextView
             android:layout_width="90dp"
             android:layout_height="wrap_content"
             android:layout_gravity="center_vertical"
             android:textSize="18sp"
             android:text="Password:" />
         <EditText
             android:id="@+id/passwordEdit"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
             android:layout_gravity="center_vertical"
             android:inputType="textPassword" />
     </LinearLayout>
    
     <LinearLayout
         android:orientation="horizontal"
         android:layout_width="match_parent"
         android:layout_height="wrap_content">
         <CheckBox
             android:id="@+id/rememberPass"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"/>
         <TextView
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:textSize="18sp"
             android:text="记住密码"/>
     </LinearLayout>
    
     <Button
         android:id="@+id/login"
         android:layout_width="200dp"
         android:layout_height="60dp"
         android:layout_gravity="center_horizontal"
         android:text="Login" />
    </LinearLayout>
    

    这里使用了一个新控件:CheckBox。这是一个复选框控件,用户可以通过点击的方式进行选中和取消,我们就使用这个控件来表示用户是否需要记住密码。

  2. 然后修改LoginActivity中的代码,如下所示:

    class LoginActivity : BaseActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_login)
    
         // 关键代码开始
         val prefs = getPreferences(Context.MODE_PRIVATE)
         val isRemember = prefs.getBoolean("remember_password", false)
         val account = prefs.getString("account", "")
         val password = prefs.getString("password", "")
         val accountEdit:EditText = findViewById(R.id.accountEdit)
         val passwordEdit:EditText = findViewById(R.id.passwordEdit)
         val rememberPass:CheckBox = findViewById(R.id.rememberPass)
         if(isRemember) {
             // 将账号和密码都设置到文本框中
             accountEdit.setText(account)
             passwordEdit.setText(password)
             rememberPass.isChecked = true
         }
         // 关键代码结束
    
         val login = findViewById<Button>(R.id.login)
         login.setOnClickListener {
             val account = accountEdit.text.toString()
             val password = passwordEdit.text.toString()
    
             // 关键代码开始
             if (account == "admin" && password == "123456"){
                 val editor = prefs.edit()
                 // 检查复选框是否被选中
                 if(rememberPass.isChecked){
                     editor.putBoolean("remember_password",true)
                     editor.putString("account",account)
                     editor.putString("password",password)
                 }else{
                     editor.clear()
                 }
                 editor.apply()
                 // 关键代码结束
    
                 val intent = Intent(this,MainActivity::class.java)
                 startActivity(intent)
                 finish()
             }else{
                 Toast.makeText(this,"账号或密码错误", Toast.LENGTH_SHORT).show()
             }
         }
     }
    }
    

    这里在onCreate()中获取了SharedPreferences对象,然后调用它的getBoolean()方法获取remember_password这个键对应的值,默认不存在对应的值,所以默认值为false.在账号密码匹配成功时检查remember_password是否被选中,如果选中了就将账号和密码保存到SharedPreferences文件中且提交,否则就调用clear()将SharedPreferences文件中的所有数据清除掉.

  3. 现在重新运行一下程序,可以看到界面上多了一个记住密码复选框,然后账号输入admin,密码输入123456,并选中记住密码复选框,点击登录,就会跳转到MainActivity。接着在MainActivity中发出一条强制下线广播,会让程序重新回到登录界面,此时你会发现,账号和密码已经自动填充到界面上了,如图所示
    博客第一行代码3学习SharedPreferences记住密码效果图

    SQLite数据库存储

    SQLite概述

    SQLite是一款轻量级的关系型数据库,它的运算速度非常快占用资源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务,所以只要你以前使用过其他的关系型数据库,就可以很快地上手SQLite。而SQLite又比一般的数据库要简单得多,它甚至不用设置用户名和密码就可以使用。Android正是把这个功能极为强大的数据库嵌入到了系统当中,使得本地持久化的功能有了一次质的飞跃。

    引言

    前面我们所学的文件存储和SharedPreferences存储毕竟只适用于保存一些简单的数据和键值对,当需要存储大量``复杂关系型数据的时候,你就会发现以上两种存储方式很难应付得了。比如我们手机的短信程序中可能会有很多个会话,每个会话中又包含了很多条信息内容,并且大部分会话还可能各自对应了通讯录中的某个联系人。很难想象如何用文件或者SharedPreferences来存储这些数据量大、结构性复杂的数据吧?但是使用数据库就可以做得到,那么我们就赶快来看一看,Android中的SQLite数据库到底是如何使用的。

    SQLiteOpenHelper

    Android提供了一个SQLiteOpenHelper``抽象帮助类,借助这个类可以非常简单地对数据库进行创建升级。如果要使用它我们就需要创建自己的帮助类去继承它,SQLiteOpenHelper中有两个抽象方法:onCreate()onUpgrade(),我们可以通过重写它来实现创建和升级数据库的逻辑.
    SQLiteOpenHelper中还有两个重要的实例方法:getReadableDatabase()getReadableDatabase(),它们都用于打开或创建数据库,如果数据库不存在则创建一个新的,且返回一个可对数据库进行读写的对象.区别就是当数据库不可写入时,getReadableDatabase()只会以只读方式打开数据库,而getWritableDatabase()方法则将抛出异常.
    SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少的那个.这个构造方法接收4个参数:
  • 参数一: 接收Context
  • 参数二: 数据库名,创建数据库时使用
  • 参数三: 查询数据库返回的自定义的Cursor,一般传入null即可
  • 参数四: 当前数据库版本号,用于对数据库进行升级操作.
    构造出SQLiteOpenHelper后调用它的getReadableDatabase()或getWritableDatabase()方法就能够创建数据库了,数据库文件会存放在/data/data/<package name>/databases/目录下。此时,重写的onCreate()方法也会得到执行,所以通常会在这里处理一些创建表的逻辑

SQLite基本数据类型

SQLite不像其他的数据库拥有众多繁杂的数据类型,它的数据类型很简单:

  • integer: 整型
  • real: 浮点型
  • text: 文本型
  • blob: 二进制型

创建数据库

在这里我们先创建一个表,Sql语句如下:

create table Book (
 id integer primary key autoincrement,
 author text,
 price real,
 pages integer,
 name text)
  1. 首先新建一个DatabaseTest项目
  2. 新建MyDatabaseHelper类继承自SQLiteOpenHelper,代码如下所示:

    class MyDatabaseHelper(val context: Context,name:String,version:Int) : SQLiteOpenHelper(context,name,null,version){
     private val SQL = "create table Book (\n" +
             " id integer primary key autoincrement,\n" +
             " author text,\n" +
             " price real,\n" +
             " pages integer,\n" +
             " name text)"
     override fun onCreate(db: SQLiteDatabase?) {
         db?.execSQL(SQL)
         Toast.makeText(context,"创建成功",Toast.LENGTH_SHORT).show()
     }
    
     override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
         TODO("Not yet implemented")
     }
    }
    

    可以看到我们在onCreate()中调用execSQL()方法创建表语句,且用Toast告知创建成功.

  3. 现在修改activity_main.xml中的代码,如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
     <Button
         android:id="@+id/createDatabase"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="创建数据库"/>
    </LinearLayout>
    
    布局文件很简单,就是加入了一个按钮,用于创建数据库。
  4. 最后修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
    
         val createDatabase = findViewById<Button>(R.id.createDatabase)
         val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)
         createDatabase.setOnClickListener{
             dbHelper.writableDatabase
         }
     }
    }
    

    我们在onCreate()方法中构建了一个MyDatabaseHelper对象,并且通过构造函数的参数将数据库名指定为BookStore.db,版本号指定为1,然后在创建数据库按钮的点击事件里调用了getWritableDatabase()方法。这样当第一次点击创建数据库按钮时,就会检测到当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用MyDatabaseHelper中的onCreate()方法,这样Book表也就创建好了,然后会弹出一个Toast提示创建成功。再次点击创建数据库按钮时,会发现此时已经存在BookStore.db数据库了,因此不会再创建一次。

  5. 现在就可以运行一下代码了,在程序主界面点击创建数据库按钮,结果如图所示
    博客第一行代码3学习SQLiteOpenHelper创建数据库1

使用App Inspection查看db文件

如果想看到是否创建成功我们仍然可以使用用Device File Explorer,但只能看见一个db文件,无法看见表里面具体的值.
想要查看db文件,可以使用最新版本的Android Studio自带的查看工具,具体使用方法如下(这是补充教程,下面可以使用这个方法替换heidisql):

  1. 点击View->Tool Windows->App Inspection
    博客第一行代码三打开AppInspection
  2. 重新安装程序,会发现无法检测到数据库,需要点击创建数据库,然后选中它即可
    博客第一行代码三使用AppInspection打开db文件
  3. 实时监测数据库,点击Live updates即可,或者手动点击刷新.注意:实时刷新只能只读.
    博客第一行代码三使用AppInspection实时监测db文件
  4. 使用Sql语句
    博客第一行代码三使用AppInspection使用SQl
  5. 使用注解,离线查询等参考官方文档

使用heidisql查看db文件

也可以安装第三方软件如:heidisql等只要是支持SQLite即可,heidisql为例

  1. 打开Device File Explorer进入data/data/com.example.databasetest/databases/目录,将BookStore.db文件导出,这里我保存到下载目录
    博客第一行代码3学习SQLite导出db文件
  2. 使用heidisql打开SQLite文件
    博客第一行代码3学习heidisSql导入db文件
    可以看到,BookStore.db数据库中确实存在了一张Book表,并且Book表中的列也和我们前面使用的建表语句完全匹配,由此证明BookStore.db数据库和Book表确实已经创建成功了

升级数据库

在刚刚创建MyDatabaseHelper的时候有一个为onUpgrade()的方法就是用于数据库升级的.
这里我们来简单在项目中再新增一张Category表用于记录图书的分类.

  1. 比如Category表中有id(主键)、分类名和分类代码这几个列,那么建表语句就可以写成:
    create table Category (
    id integer primary key autoincrement,
    category_name text,
    category_code integer)
    
  2. 接下来我们将这条建表语句添加到MyDatabaseHelper中,代码如下所示:

    class MyDatabaseHelper(val context: Context,name:String,version:Int) : SQLiteOpenHelper(context,name,null,version){
     private val SQL = "create table Book (\n" +
             " id integer primary key autoincrement,\n" +
             " author text,\n" +
             " price real,\n" +
             " pages integer,\n" +
             " name text)"
     private val SQL2 = "create table Category (\n" +
             " id integer primary key autoincrement,\n" +
             " category_name text,\n" +
             " category_code integer)"
     override fun onCreate(db: SQLiteDatabase?) {
         db?.execSQL(SQL)
         db?.execSQL(SQL2)
         Toast.makeText(context,"创建成功",Toast.LENGTH_SHORT).show()
     }
    
     override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
         TODO("Not yet implemented")
     }
    }
    

    欸!运行了发现Category表并没有创建,因为BookStore.db数据库已经存在了,onCreate并不会被触发.

  3. 有了上面错误的示范,现在我们用SQLiteOpenHelper的升级功能.修改MyDatabaseHelper中的代码,如下所示:

    class MyDatabaseHelper(val context: Context,name:String,version:Int) : SQLiteOpenHelper(context,name,null,version){
     private val SQL = "create table Book (\n" +
             " id integer primary key autoincrement,\n" +
             " author text,\n" +
             " price real,\n" +
             " pages integer,\n" +
             " name text)"
     private val SQL2 = "CREATE TABLE IF NOT EXISTS Category (\n" +
             " id integer primary key autoincrement,\n" +
             " category_name text,\n" +
             " category_code integer)"
     override fun onCreate(db: SQLiteDatabase?) {
         db?.execSQL(SQL)
         Toast.makeText(context,"创建成功",Toast.LENGTH_SHORT).show()
     }
    
     override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
    
         db?.execSQL(SQL2)
         Toast.makeText(context,"新增数据库成功",Toast.LENGTH_SHORT).show()
    
     }
    }
    

    现在已经再onUpgrade()写好了代码,接下来就是如何触发更新.

  4. SQLiteOpenHelper的构造方法里接收的第四个参数表示数据库版本号,只要修改它就能触发onUpgrade()方法.修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val createDatabase = findViewById<Button>(R.id.createDatabase)
    
         val dbHelper = MyDatabaseHelper(this,"BookStore.db",2)
    
         createDatabase.setOnClickListener{
             dbHelper.writableDatabase
         }
     }
    }
    

    这里版本号指定为2

  5. 重新运行程序点击创建数据库按钮,这是就会弹出升级成功的提示,然后打开db文件查看.如下所示:
    博客第一行代码3学习SQLite升级数据库效果图
    博客第一行代码3学习SQLikotlinxte升级数据库效果图2
    可以看到,Category表已经创建成功了,说明我们的升级功能的确起到了作用。

SQLite的增删改查(CRUD)

数据库最常用的无非就是CURD,每一种操作都对应着一种SQL命令.但对于不熟悉SQL的人来说这无疑是陌生的,因此Android提供了一系列的辅助性方法,让开发者不必编写SQL语句也能完成CRUD操作.
在前面讲过,调用SQLiteOpenHelper的getReadableDatabas()或getWritableDatabase()方法是会返回一个SQLiteDatabase对象,在这个对象中就有着CRUD对应的方法.下面就对这几个方法进行讲解

insert()方法(增加数据)

insert()接收3个参数:

  • 参数一: 被新增数据的表名
  • 参数二: 未指定添加数据的列自动赋值为null,一般传入null即可
  • 参数三: 需要一个ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues中添加数据,一般传入需要添加的列名和数据即可.

下面我们来举个例子试试看:

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

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
     <Button
         android:id="@+id/createDatabase"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="创建数据库"/>
    
     <Button
         android:id="@+id/addData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="添加数据"/>
    </LinearLayout>
    

    我们在布局文件中又新增了一个按钮,稍后就会在这个按钮的点击事件里编写添加数据的逻辑。

  2. 接着修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val createDatabase = findViewById<Button>(R.id.createDatabase)
         val dbHelper = MyDatabaseHelper(this,"BookStore.db",2)
         createDatabase.setOnClickListener{
             dbHelper.writableDatabase
         }
    
         val addData = findViewById<Button>(R.id.addData)
         addData.setOnClickListener{
             val db = dbHelper.writableDatabase
             val values1 = ContentValues().apply {
                 put("name", "第一行代码3")
                 put("author", "郭霖")
                 put("pages", 810)
                 put("price",9.9)
             }
             db?.insert("Book",null,values1)
             val values2 = ContentValues().apply {
                 put("name","第二行代码")
                 put("author","明月清风")
                 put("pages",233)
                 put("price",10)
             }
             db?.insert("Book",null,values2)
         }
     }
    }
    

    在添加数据按钮的点击事件里,我们先获取了SQLiteDatabase对象,然后使用ContentValues对要添加的数据进行组装。如果你比较细心的话,应该会发现这里只对Book表里其中4列的数据进行了组装,id那一列并没给它赋值。这是因为在前面创建表的时候,我们就将id列设置为自增长了,它的值会在入库的时候自动生成,所以不需要手动赋值了。接下来调用了insert()方法将数据添加到表当中,注意这里我们添加了两条数据。

  3. 现在可以重新运行一下程序了,界面如图所示
    博客第一行代码3学习SQLite学习Inster效果图1
    然后导出db文件,可以看见新的数据已被插入,图片如下:
    博客第一行代码3学习SQLite学习Inster效果图2

update()方法(更新数据)

update()方法,用于对数据进行更新,该方法接收4个参数:

  • 参数一: 被更新的表名
  • 参数二: ContentValues对象,把要更新的数据在这里组装进去.
  • 参数三、四: 约束变更新一行或某几行中的数据,不指定则默认更新所有行.

接下来我们给刚刚添加的书修改以下价格.

  1. 首先修改activity_main.xml中的代码,如下所示:
    ```xml
    <?xml version=”1.0” encoding=”utf-8”?>

    <Button

     android:id="@+id/createDatabase"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:text="创建数据库"/>
    

</LinearLayout>

布局文件中的代码已经非常简单了,就是添加了一个用于更新数据的按钮。
2. 修改`MainActivity`中的代码,如下所示:
```kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val createDatabase = findViewById<Button>(R.id.createDatabase)
        val dbHelper = MyDatabaseHelper(this,"BookStore.db",2)
        createDatabase.setOnClickListener{
            dbHelper.writableDatabase
        }
        val addData = findViewById<Button>(R.id.addData)
        addData.setOnClickListener{
            val db = dbHelper.writableDatabase
            val values1 = ContentValues().apply {
                put("name", "第一行代码3")
                put("author", "郭霖")
                put("pages", 810)
                put("price",9.9)
            }
            db?.insert("Book",null,values1)
            val values2 = ContentValues().apply {
                put("name","第二行代码")
                put("author","明月清风")
                put("pages",233)
                put("price",10)
            }
            db?.insert("Book",null,values2)
        }

        val updateData = findViewById<Button>(R.id.updateData)
        updateData.setOnClickListener {
            val db = dbHelper.writableDatabase
            val values = ContentValues().apply {
                put("price", "100")
            }
            db?.update("Book",values,"name = ?",arrayOf("第二行代码"))
        }
    }
}

这里在更新数据按钮的点击事件里面构建了一个ContentValues对象,并且只给它指定了一组数据,说明我们只是想把价格这一列的数据更新成100。然后调用了SQLiteDatabaseupdate()方法执行具体的更新操作,可以看到,这里使用了第三第四个参数来指定具体更新哪几行第三个参数对应的是SQL语句的where部分,表示更新所有name等于?的行,而?是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容,arrayOf()方法是Kotlin提供的一种用于便捷创建数组的内置方法。因此上述代码想表达的意图就是将第二行代码这本书的价格改成100

  1. 现在重新运行一下程序,界面如图所示。
    博客第一行代码3学习SQLite更新数据库效果图1
    打开db查看
    博客第一行代码3学习SQLite更新数据库效果图2

delete()方法(删除数据)

delete()方法,用于对数据进行删除,该方法接收3个参数:

  • 参数一: 被删除的表名
  • 参数二、三: 约束删除某一行或几行的数据,不指定的话默认删除所有行.

接下来我们就试着操作一下:

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

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
     <Button
         android:id="@+id/createDatabase"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="创建数据库"/>
     <Button
         android:id="@+id/addData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="添加数据"/>
     <Button
         android:id="@+id/updateData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="更新数据"/>
    
     <Button
         android:id="@+id/deleteData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="删除数据"/>
    </LinearLayout>
    

    仍然是在布局文件中添加了一个按钮,用于删除数据。

  2. 然后修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val createDatabase = findViewById<Button>(R.id.createDatabase)
         val dbHelper = MyDatabaseHelper(this,"BookStore.db",2)
         createDatabase.setOnClickListener{
             dbHelper.writableDatabase
         }
         val addData = findViewById<Button>(R.id.addData)
         addData.setOnClickListener{
             val db = dbHelper.writableDatabase
             val values1 = ContentValues().apply {
                 put("name", "第一行代码3")
                 put("author", "郭霖")
                 put("pages", 810)
                 put("price",9.9)
             }
             db?.insert("Book",null,values1)
             val values2 = ContentValues().apply {
                 put("name","第二行代码")
                 put("author","明月清风")
                 put("pages",233)
                 put("price",10)
             }
             db?.insert("Book",null,values2)
         }
         val updateData = findViewById<Button>(R.id.updateData)
         updateData.setOnClickListener {
             val db = dbHelper.writableDatabase
             val values = ContentValues().apply {
                 put("price", "100")
             }
             db?.update("Book",values,"name = ?",arrayOf("第二行代码"))
         }
    
         val deleteData = findViewById<Button>(R.id.deleteData)
         deleteData.setOnClickListener{
             val db = dbHelper.writableDatabase
             db?.delete("Book","pages > ?", arrayOf("800"))
         }
     }
    }
    

    可以看到,我们在删除按钮的点击事件里指明删除Book表中的数据,并且通过第二、第三个参数来指定仅删除那些页数超过800页的书。当然这个需求很奇怪,这里仅仅是为了做个测试。你可以先查看一下当前Book表里的数据,其中第一行代码3 这本书的页数超过了800页,也就是说当我们点击删除按钮时,这条记录应该会被删除。

  3. 现在重新运行一下程序,界面如图所示。
    博客第一行代码3学习SQLite删除数据库效果图1
    打开db文件查看
    博客第一行代码3学习SQLite删除数据库效果图2

query()方法(查询数据)

SQLiteDatabase中还提供了一个query()方法用于对数据进行查询。这个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。

  • 参数一: 指定被查询的表名
  • 参数二: 指定查询那几列的列名,默认查询所有列
  • 参数三、四: 约束查询某一行或几行的数据,where表达式和表达式中占位符中的值
  • 参数五: 指定group by的列,不指定则不进行group by操作
  • 参数六: group by后的having约束
  • 参数七: 查询结果的排序,不指定默认排序

博客第一行代码3学习SQLite的query查询具体含义

可以看到query()方法有不少参数都有默认值,所以多数情况下只有传入几个参数即可.调用query()方法会返回一个Cursor对象,查询的所有数据都从这里取出.

下面还是让我们通过具体的例子来体验一下查询数据的用法

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

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
     <Button
         android:id="@+id/createDatabase"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="创建数据库"/>
     <Button
         android:id="@+id/addData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="添加数据"/>
     <Button
         android:id="@+id/updateData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="更新数据"/>
     <Button
         android:id="@+id/deleteData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="删除数据"/>
    
     <Button
         android:id="@+id/queryData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="查询数据"/>
    </LinearLayout>
    

    添加了一个按钮用于查询数据

  2. 然后修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val createDatabase = findViewById<Button>(R.id.createDatabase)
         val dbHelper = MyDatabaseHelper(this,"BookStore.db",2)
         createDatabase.setOnClickListener{
             dbHelper.writableDatabase
         }
         val addData = findViewById<Button>(R.id.addData)
         addData.setOnClickListener{
             val db = dbHelper.writableDatabase
             val values1 = ContentValues().apply {
                 put("name", "第一行代码3")
                 put("author", "郭霖")
                 put("pages", 810)
                 put("price",9.9)
             }
             db?.insert("Book",null,values1)
             val values2 = ContentValues().apply {
                 put("name","第二行代码")
                 put("author","明月清风")
                 put("pages",233)
                 put("price",10)
             }
             db?.insert("Book",null,values2)
         }
         val updateData = findViewById<Button>(R.id.updateData)
         updateData.setOnClickListener {
             val db = dbHelper.writableDatabase
             val values = ContentValues().apply {
                 put("price", "100")
             }
             db?.update("Book",values,"name = ?",arrayOf("第二行代码"))
         }
         val deleteData = findViewById<Button>(R.id.deleteData)
         deleteData.setOnClickListener{
             val db = dbHelper.writableDatabase
             db?.delete("Book","pages > ?",arrayOf("800"))
         }
    
         val queryData = findViewById<Button>(R.id.queryData)
         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.getColumnIndexOrThrow("name"))
                     val author = cursor.getString(cursor.getColumnIndexOrThrow("author"))
                     val pages = cursor.getString(cursor.getColumnIndexOrThrow("pages"))
                     val price = cursor.getString(cursor.getColumnIndexOrThrow("price"))
                     Log.d("MainActivity","book name is $name")
                     Log.d("MainActivity","book name is $author")
                     Log.d("MainActivity","book name is $pages")
                     Log.d("MainActivity","book name is $price")
                 }while (cursor.moveToNext())
             }
             cursor.close()
         }
     }
    }
    

    我们在查询按钮的点击事件里调用了SQLiteDatabasequery()方法查询数据。这里的query()第一个参数指明查询Book表,后面的参数全部为null,表示希望查询这张表中的所有数据.查询后就得到了一个Cursor对象,接着调用它的moveToFirst()方法,将数据的指针移动到第一行的位置,然后继续循环去遍历每一行数据.在这个Cursor的getColumnIndexOrThrow()方法获取某一列在表中对应的位置索引,然后将索引传入相应的取值中,就可以得到从数据库中读取到数据了,最后使用close()方法来关闭Cursor.

  3. 好了,现在重新运行程序,界面如图所示。
    博客第一行代码3学习SQLite点击查询数据按钮效果图

使用SQL操作数据库

如果不想使用Android提供的方法,Android也考虑到了这种情况,同样提供了一系列的方法可以直接使用SQL来操作数据库.

  1. 使用execSQL()
    添加数据
    db?.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
                 arrayOf("第三行代码","xxx","454","2333")
             )
    
  2. 使用rawQuery()
    db?.rawQuery("select * from Book", null)
    
    可以看到,除了查询数据的时候调用的是SQLiteDatabase的rawQuery()方法,其他操作都
    是调用的execSQL()方法。以上演示的几种方式的执行结果会和前面我们学习的CRUD操作的
    结果完全相同,选择使用哪一种方式就看你个人的喜好了。

SQLite数据库的最佳实践

本节将介绍SQLite的更多高级用法.

使用事务

事务的具体含义可以看其它文章,这里不做过多介绍.
接下来我们看一看如何在Android中使用事务吧,仍然是在DatabaseTest项目的基础上进行修改。

引言

比如Book表中的数据已经很老了,现在准备全部废弃,替换成新数据,可以先使用delete()方法将Book表中的数据删除,然后再使用insert()方法将新的数据添加到表中。我们要保证删除旧数据和添加新数据的操作必须一起完成,否则就要继续保留原来的旧数据。

实现

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

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
     <Button
         android:id="@+id/createDatabase"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="创建数据库"/>
     <Button
         android:id="@+id/addData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="添加数据"/>
     <Button
         android:id="@+id/updateData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="更新数据"/>
     <Button
         android:id="@+id/deleteData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="删除数据"/>
     <Button
         android:id="@+id/queryData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="查询数据"/>
    
     <Button
         android:id="@+id/replaceData"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="替换数据"/>
    </LinearLayout>
    

    可以看到,这里又添加了一个按钮,用于进行数据替换操作。

  2. 然后修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val createDatabase = findViewById<Button>(R.id.createDatabase)
         val dbHelper = MyDatabaseHelper(this,"BookStore.db",2)
         createDatabase.setOnClickListener{
             dbHelper.writableDatabase
         }
         val addData = findViewById<Button>(R.id.addData)
         addData.setOnClickListener{
             val db = dbHelper.writableDatabase
             val values1 = ContentValues().apply {
                 put("name", "第一行代码3")
                 put("author", "郭霖")
                 put("pages", 810)
                 put("price",9.9)
             }
             db?.insert("Book",null,values1)
             val values2 = ContentValues().apply {
                 put("name","第二行代码")
                 put("author","明月清风")
                 put("pages",233)
                 put("price",10)
             }
             db?.insert("Book",null,values2)
         }
         val updateData = findViewById<Button>(R.id.updateData)
         updateData.setOnClickListener {
             val db = dbHelper.writableDatabase
             val values = ContentValues().apply {
                 put("price", "100")
             }
             db?.update("Book",values,"name = ?",arrayOf("第二行代码"))
         }
         val deleteData = findViewById<Button>(R.id.deleteData)
         deleteData.setOnClickListener{
             val db = dbHelper.writableDatabase
             db?.delete("Book","pages > ?",arrayOf("800"))
         }
         val queryData = findViewById<Button>(R.id.queryData)
         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.getColumnIndexOrThrow("name"))
                     val author = cursor.getString(cursor.getColumnIndexOrThrow("author"))
                     val pages = cursor.getString(cursor.getColumnIndexOrThrow("pages"))
                     val price = cursor.getString(cursor.getColumnIndexOrThrow("price"))
                     Log.d("MainActivity","book name is $name")
                     Log.d("MainActivity","book name is $author")
                     Log.d("MainActivity","book name is $pages")
                     Log.d("MainActivity","book name is $price")
                 }while (cursor.moveToNext())
             }
             cursor.close()
         }
    
         val replaceData = findViewById<Button>(R.id.replaceData)
         replaceData.setOnClickListener {
             val db = dbHelper.writableDatabase
             // 开启事务
             db.beginTransaction()
             try {
                 db.delete("Book",null,null)
                 // 手动模拟一个异常,让事务失败
                 if (true) throw java.lang.NullPointerException()
                 val values = ContentValues().apply {
                     put("name","权力的游戏")
                     put("author","乔治。马丁")
                     put("pages",720)
                     put("price",20.85)
                 }
                 db.insert("Book",null,values)
                 // 设置事务执行成功
                 db.setTransactionSuccessful()
             }catch (e: Exception) {
                 e.printStackTrace()
             }finally {
                 // 结束事务
                 db.endTransaction()
             }
         }
     }
    }
    

    上述代码就是Android中事务的标准用法,首先调用SQLiteDatabasebeginTransaction()方法开启一个事务,然后在一个异常捕获的代码块中执行具体的数据库操作,当所有的操作都完成之后,调用setTransactionSuccessful()表示事务已经执行成功了,最后在finally代码块中调用endTransaction()结束事务。注意观察,我们在删除旧数据的操作完成后手动抛出了一个NullPointerException,这样添加新数据的代码就执行不到了。不过由于事务的存在,中途出现异常会导致事务的失败,此时旧数据应该是删除不掉的。

  3. 现在运行一下程序并点击“Replace Data”按钮,然后点击“Query Data”按钮。你会发现,Book表中存在的还是之前的旧数据,说明我们的事务确实生效了。

  4. 然后将手动抛出异常的那行代码删除并重新运行程序,此时点击一下“Replace Data”按钮,就会将Book表中的数据替换成新数据了,你可以再使用“Query Data”按钮来验证一次。

升级数据库的最佳写法

引言

前面我们学习的升级数据库的方式是非常粗暴的,为了保证数据库中的表是最新的,我们只是简单地在onUpgrade()方法中删除掉了当前所有的表,然后强制重新执行了一遍onCreate()方法。这种方式在产品的开发阶段确实可以用,但是当产品真正上线之后就绝对不行了。想象以下场景,比如你编写的某个应用已经成功上线了,并且还拥有了不错的下载量。现在由于添加了新功能,数据库需要一起升级,结果用户更新了这个版本之后却发现以前程序中存储的本地数据全部丢失了!那么很遗憾,你的用户群体可能已经流失一大半了。听起来好像挺恐怖的样子,难道在产品发布出去之后还不能升级数据库了?当然不是,其实只需要进行一些合理的控制,就可以保证在升级数据库的时候数据并不会丢失了。
下面我们就来学习一下如何实现这样的功能。你已经知道,每一个数据库版本都会对应一个版本号,当指定的数据库版本号大于当前数据库版本号的时候,就会进入onUpgrade()方法中执行更新操作。这里需要为每一个版本号赋予其所对应的数据库变动,然后在onUpgrade()方法中对当前数据库的版本号进行判断,再执行相应的改变就可以了。
下面就让我们模拟一个数据库升级的案例,还是由MyDatabaseHelper类对数据库进行管理。第1版的程序要求非常简单,只需要创建一张Book表

  1. MyDatabaseHelper中的代码如下所示:

    class MyDatabaseHelper(val context: Context,name:String,version:Int) : SQLiteOpenHelper(context,name,null,version){
     private val createBook = "create table Book (\n" +
             " id integer primary key autoincrement,\n" +
             " author text,\n" +
             " price real,\n" +
             " pages integer,\n" +
             " name text)"
     override fun onCreate(db: SQLiteDatabase?) {
         db?.execSQL(createBook)
     }
    
     override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
     }
    }
    
  2. 几星期之后又有了新需求,这次需要向数据库中再添加一张Category表。于是,修改MyDatabaseHelper中的代码,如下所示:

    class MyDatabaseHelper(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," +
             "category_id integer)" // 这里新增了一句sql
    
     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)
         }
     }
    }
    

    可以看到,在onCreate()方法里我们新增了一条建表语句,然后又在onUpgrade()方法中添加了一个if判断,如果用户数据库的旧版本号小于等于1,就只会创建一张Category表。
    这样当用户直接安装第2版的程序时,就会进入onCreate()方法,将两张表一起创建。而当用户使用第2版的程序覆盖安装第1版的程序时,就会进入升级数据库的操作中,此时由于Book表已经存在了,因此只需要创建一张Category表即可。

  3. 但是没过多久,新的需求又来了,这次要给Book表和Category表之间建立关联,需要在Book表中添加一个category_id字段。再次修改MyDatabaseHelper中的代码,如下所示:

    class MyDatabaseHelper(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," +
             "category_id integer)" // 这里新增了一句sql
    
     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)
         }
         if (oldVersion <= 2) {
             db?.execSQL("alter table Book add column category_id integer")
         }
     }
    }
    

    首先我们在Book表的建表语句中添加了一个category_id列,这样当用户直接安装第3版的程序时,这个新增的列就已经自动添加成功了。
    如果用户之前已经安装了某一版本的程序,现在需要覆盖安装,就会进入升级数据库的操作中。在onUpgrade()方法里,我们添加了一个新的条件,如果当前数据库的版本号是2,就会执行alter命令,为Book表新增一个category_id列。
    这里请注意一个非常重要的细节:每当升级一个数据库版本的时候,onUpgrade()方法里都一定要写一个相应的when条件。这是为了保证App在跨版本升级的时候,每一次的数据库修改都能被全部执行。比如用户当前是从第2版升级到第3版,那么只有第二条判断语句会执行,而如果用户是直接从第1版升级到第3版,那么两条判断语句都会执行。使用这种方式来维护数据库的升级,不管版本怎样更新,都可以保证数据库的表结构是最新的,而且表中的数据完全不会丢失。

而实际上现在Google又推出了一个专门用于Android平台的数据库框架——Room。相比于传统的数据库API,Room的用法要更加复杂一些,但是却更加科学和规范,也更加符合现代高质量App的开发标准,我们将在第13章中学习这部分内容。

Kotlin课堂:高阶函数的应用

在上一章的Kotlin课堂中,我们学习了高阶函数应该如何使用,而本章的Kotlin课堂里,我们将会学习高阶函数具体可以用在哪里。
高阶函数非常适用于简化各种API的调用,一些API的原有用法在使用高阶函数简化之后,不管是在易用性还是可读性方面,都可能会有很大的提升。
为了进行举例说明,我们在本节Kotlin课堂里会使用高阶函数简化SharedPreferences和ContentValues这两种API的用法,让它们的使用变得更加简单。

简化SharedPreferences的用法

点击这里回顾SharedPreferences
SharedPreferences存储数据大致可以分为3步:

  1. 调用SharedPreferences的edit()方法获取SharedPreferences.Editor对象;
  2. 向SharedPreferences.Editor对象中添加数据;
  3. 调用apply()方法将添加的数据提交,完成数据存储操作;
    对应的代码大致如下:
    val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
    editor.putString("name", "Tom")
    editor.putInt("age", 28)
    editor.putBoolean("married", false)
    editor.apply()
    

这段代码本身已经够简单了,但是在Kotlin的高阶函数中还能再简化它.

  1. 新建一个SharedPreferences.kt文件,然后在里面加入如下代码:
    fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit){
     val editor = edit()
     editor.block()
     editor.apply()
    }
    
    我们通过拓展函数的方式向SharedPreferences类中添加了一个open函数,且接收一个函数参数类型的参数.
    open函数内自动拥有SharedPreferences的上下文,所以可以调用edit()方法来获得SharedPreferences.Editor对象.open函数接收的是一个SharedPreferences.Editor的函数参数,因此需要调用editor.block().
  2. 定义open函数后,我们在项目中使用SharedPreferences存储数据就会更会方便,写法如下:
    getSharedPreferences("data", Context.MODE_PRIVATE).open {
     putString("name", "Tom")
     putInt("age", 28)
     putBoolean("married", false)
    }
    
    我们可以直接在SharedPreferences对象上调用open函数,然后在Lambda表达式中完成数据的添加操作。
    Lambda表达式拥有的是SharedPreferences.Editor的上下文环境,因此这里可以直接调用相应的put方法来添加数据。
    最后我们也不再需要调用apply()方法来提交数据了,因为open函数会自动完成提交操作。

    其实Google提供的KTX扩展库中已经包含了上述SharedPreferences的简化用法,这个扩展库会在Android Studio创建项目的时候自动引入build.gradle的dependencies中
    博客第一行代码3学习Google自动导入的ktx库
    因此,我们实际上可以直接在项目中使用如下写法来向SharedPreferences存储数据:

    getSharedPreferences("data", Context.MODE_PRIVATE).edit {
    putString("name", "Tom")
    putInt("age", 28)
    putBoolean("married", false)
    }
    

简化ContentValues的用法

引言

ContentValues的基本用法在SQLite的增删改查(CRUD))中已经学过了,它主要结合SQLiteDatabase的API存储和修改数据库的数据,具体用法示例如下:

val values = ContentValues()
values.put("name", "Game of Thrones")
values.put("author", "George Martin")
values.put("pages", 720)
values.put("price", 20.85)
db.insert("Book", null, values)

可能你会发现这段代码可以使用applay进行简化,即便如此我们还可以做到更好.
在简化之前我们还需要了解mapOf()方法
在Kotlin中使用A to B这样的语法结构会创建一个Pair对象,暂时你只需要知道这些就可以了,至于为什么,我们将在第9章的Kotlin课堂中学习。

初步优化

  1. 新建一个ContentValues.kt文件,然后在里面定义一个cvOf()方法,如下所示:
    fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {
     TODO("代码")
    }
    
    介个方法继承ContentValues,cvOf()方法接收了一个Pair参数,也就是A to B的参数.vararg关键字对应着Java的可变参数列表,允许我们传入任意个数Pair类型的参数,这些参数都会被赋值到使用vararg声明的这一个变量上面,使用for-in循环可以将它传入的参数遍历出来.
    Pair是一种键值对的数据结构,需要指定它的键和值的类型.值得庆幸的是ContentValues所有的键都是字符串类型的,但值却是多种多样的.因此,我们需要将值的类型指定为Any?相当于Java的Object.
  2. 为cvOf()实现核心代码
    fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {
     val cv = ContentValues()
     for (pair in pairs){
         val key = pair.first
         val value = pair.second
         when (value) {
             is Int -> cv.put(key, value)
             is Long -> cv.put(key, value)
             is Short -> cv.put(key, value)
             is Float -> cv.put(key, value)
             is Double -> cv.put(key, value)
             is Boolean -> cv.put(key, value)
             is String -> cv.put(key, value)
             is Byte -> cv.put(key, value)
             is ByteArray -> cv.put(key, value)
             null -> cv.putNull(key)
         }
     }
     return cv
    }
    
    核心思路就是先创建一个ContentValues对象,然后遍历pairs参数列表,读出数据并填入ContentValues中,最后返回ContentValues对象.
    可能会注意高阶函数到,这里虽然进行了类型判断,但是后面执行的代码却是一样的.这是使用了Kotlin的Smart casts功能,比如当when语句进入Int分支后,这个条件下的value会被自动转为Int类型,而不再是Any?类型,这样就不需要再进行转型,这个特性在if中也同样适用.
  3. 经过改造的ContentValues在使用时会变得更简单,比如向数据库中添加一条数据.
    val values = cvOf("name" to "Game of Thrones", "author" to "George Martin","pages" to 720, "price" to 20.85)
    db?.insert("Book",null,values)
    
    上面的cvOf会返回一个ContentValues对象,恰巧insert需要的就是一个values需要的就是一个ContentValues对象.

    配合apply进一步优化

    从功能性方面,cvOf()方法好像确实用不到高阶函数的知识,但是从代码实现方面,却可以结合高阶函数来进行进一步的优化。比如借助apply函数,cvOf()方法的实现将会变得更加优雅:
  4. 代码如下
    fun cvOf(vararg pairs: Pair<String, Any?>)=ContentValues().apply{
     for (pair in pairs){
         val key = pair.first
         val value = pair.second
         when (value) {
             is Int -> put(key, value)
             is Long -> put(key, value)
             is Short -> put(key, value)
             is Float -> put(key, value)
             is Double -> put(key, value)
             is Boolean -> put(key, value)
             is String -> put(key, value)
             is Byte -> put(key, value)
             is ByteArray -> put(key, value)
             null ->putNull(key)
         }
     }
    }
    
    和之前的代码相比,去掉了内部定义的ContentValues对象.这是因为使用apply后它的Lambda表达式内部的this就是ContentValues,也就可以直接调用put方法,最后返回的也是它本身.

最后指的一说的是,KTX库中也提供了具有相同功能的方法!!!

评论