提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方,欢迎各位在评论中指出。
文章目录
一、数据持久化技术
我们把那些保存在内存中的数据称作是瞬时状态数据,而保存在存储设备中的数据则称为持久状态数据。由于瞬时数据很可能因为程序关闭或者内存被回收而导致数据丢失。为了解决这个问题,我们需要借助数据持久化技术来将瞬时数据保存在存储设备中。Android系统提供了3种方式来实现数据的持久化:① 文件存储 ② SharePreferences存储 ③ 数据库存储
二、文件存储
文件存储是Android中最基础的数据存储方式,它不会对存储的内容进行任何格式化处理,所有数据都是原封不动的保存到文件当中。文件存储方式适合存放一些简单的文本数据或者二进制数据。
2.1 将数据写入到文件中
Context类提供了一个openFileOutput()方法用于将数据存储到指定的文件中。openFileOutput()方法要求传入2个参数:① 文件名称 ② 文件模式。
openFileOutput()方法中的文件名参数不可以包含路径,所有的文件都默认存储在 /data/data/<包名>/files/ 目录下。openFileOutput()方法中的文件模式参数主要有:Context.MODE_PRIVATE模式和Context.MODE_APPEND模式。
- Context.MODE_PRIVATE模式:默认的操作模式。若指定的文件名已经存在,那么写入的数据会覆盖原文件中的数据。
- Context.MODE_APPEND模式:若指定的文件名已存在,就往文件里面追加内容;若文件名不存在,就创建新文件。
为了将数据写入文件中,我们需要创建FileOutputStream文件输出流对象——>OutputStreamWriter对象——>BufferedWriter对象。下面这段示例代码实现了将一段文本内容保存到一个名为data的文件中:
Kotlin Code:
fun save(inputText:String){
try{
//使用openFileOutput()打开一个FileOutputStream(文件输出流)
文件名 文件模式
val outputFileStream = openFileOutput("data", MODE_PRIVATE)
//通过FileFileOutputStream构建一个OutputStreamWriter对象
//用OutputStreamWriter对文件输出流进行包装 可以更高效地进行字符写入操作
val bufferedWriter = BufferedWriter(OutputStreamWriter(outputFileStream))
// 使用use()来自动实现流的关闭
bufferedWriter.use {
// 将字符串写入文件
it.write(inputText)
}
}catch (e:IOException){
//IO异常捕获
e.printStackTrace()
}
}
Java Code:
public void save(String inputText) {
// 使用try-with-resources确保资源自动关闭
try (FileOutputStream outputFileStream = openFileOutput("data", MODE_PRIVATE);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputFileStream))) {
// 将字符串写入文件
bufferedWriter.write(inputText);
} catch (IOException e) {
// IOException & FileNotFoundException
e.printStackTrace();
}
}
Java是通过try-with-resources语法来实现类似kotlin的use()的,它确保了在使用完资源后,资源会被自动关闭,从而避免资源泄漏问题。
try-with-resources语法结构:
try (初始化资源对象) {
使用资源
} catch (ExceptionType e) {
异常处理
}
在try块的( )中进行资源的初始化,在try块的{}中使用这些资源。Java会在try块结束时自动调用资源的close()方法,无论是否发生异常。
上面这段代码可能看起来有点复杂,我们先来学习几个概念:
【输入流与输出流】:
- 输入流:用于从文件、内存或任何形式的数据源中读取数据的模型。
- 输出流:用于将数据写入文件来实现数据的存储、传输和处理等功能的模型。
需要注意的是:必须在使用文件的输入流和输出流后正确的将流关闭。如果在使用完毕后没有正确的关闭流,则可能会导致内存泄漏或者文件被锁定等问题。
【use函数】:
- 在Kotlin中,你可以使用use函数来自动实现流的关闭。use函数接收一个Lambda表达式,该表达式接收一个输入流或输出流作为参数,然后在这个lambda表达式中我们可以安全地使用文件流。当lambda表达式执行完毕后,use函数会自动关闭文件流并释放资源。
接下来我们编写一个完整的例子开始学习吧!首先我们创建一个FilePersistenceTest项目。修改activity_main.xml主界面:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/myInputEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入内容" />
</LinearLayout>
修改MainActivity.kt文件:
Kotlin Code:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
//在销毁前将输入框的字符串保存在文件中
override fun onDestroy() {
super.onDestroy()
//获取输入框的字符串
val myInputText = myInputEdit.text.toString()
//保存到文件中
save(myInputText)
}
//按下返回键销毁当前Activity
override fun onBackPressed() {
super.onBackPressed()
finish()
}
//将字符串写入data文件
private fun save(myInputText: String) {
try {
val fileOutputStream = openFileOutput("data", MODE_PRIVATE)
val bufferedWriter = BufferedWriter(OutputStreamWriter(fileOutputStream))
bufferedWriter.use {
it.write(myInputText)
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
Java Code:
public class MainActivity extends AppCompatActivity {
ActivityMainBinding mainBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mainBinding.getRoot());
onMyBackPressed(true);
}
@Override
protected void onPause() {
super.onPause();
String myInputText = String.valueOf(mainBinding.myInputEdit.getText());
save(myInputText);
}
private void onMyBackPressed(Boolean isEnable) {
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(isEnable) {
@Override
public void handleOnBackPressed() {
finish();
}
});
}
public void save(String inputText) {
try (FileOutputStream outputFileStream = openFileOutput("data", MODE_PRIVATE);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputFileStream))) {
// 将字符串写入文件
bufferedWriter.write(inputText);
} catch (IOException e) {
// IOException & FileNotFoundException
e.printStackTrace();
}
}
}
当我们运行程序后,在输入框内输入一段文字:
按下返回键退出程序后通过Android Studio——>右下角的Device File Explore——>找到我们刚才创建的文件data:
可以看到我们在输入框内输入的内容被保存到data文件中了:
假如我们没有在onDestroy()方法中调用save()保存数据,那么我们在数据框中输入的任何内容都会在按下Back键退出程序后丢失。因为它只是瞬时数据,在Activity被销毁后就会被系统回收。但是我们通过在onDestroy()方法中调用save()方法,就可以在数据被回收之前将它存储到文件当中。
2.2 从文件中读取数据
通过上面的案例,你已经学会了如何将数据存储到文件中。在Context中还提供了一个openFIleInput()方法,用于从文件中读取数据。openFIleInput()方法只接收一个文件名作为参数,然后系统会自动到/data/data/<包名>/files/目录下加载这个文件,然后返回一个FileInputStream文件输入流对象。得到通过这个文件输入流对象,就可以将数据读取出来了。
下面这段示例代码实现了如何从一个名为data的文件中读取文本数据:
fun load(): String {
val myContent = StringBuilder()
try {
//使用openFileInput()打开一个FileInputStreamReader(文件输入流)
val inputFileStream = openFileInput("data")
//通过文件输入流构建一个InputStreamReader对象
//用BufferedReader对文件输入流进行包装 可以更高效地进行字符写入操作
val bufferedReader = BufferedReader(InputStreamReader(inputFileStream))
//use()自动实现流的关闭
bufferedReader.use {
//forEachLine()会将读取到的每行内容都回调到Lambda表达式中
bufferedReader.forEachLine {
//将每行内容进行拼接
myContent.append(it)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
//转换成字符串
return myContent.toString()
}
为了从文件中读取数据,我们需要FileInputStreamReader文件输入流对象——>InputStreamReader对象——>BufferedReader对象。接下来我们继续完善FilePersistenceTest项目,实现从文件中读取数据并恢复到文本输入框中吧:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//从data文件中读取数据
val inputText = load()
//若文件内容不为空
if (inputText.isNotBlank()) {
myInputEdit.setText(inputText) // 将文件中的数据恢复到输入框中
myInputEdit.setSelection(inputText.length) // 将输入框光标移动到文本的末尾
Toast.makeText(this, "数据已经恢复!", Toast.LENGTH_SHORT).show()
}
}
override fun onDestroy() {
· · ·
}
override fun onBackPressed() {
· · ·
}
private fun save(myInputText: String) {
· · ·
}
private fun load(): String {
val myContent = StringBuilder()
try {
val inputFileStream = openFileInput("data")
val bufferedReader = BufferedReader(InputStreamReader(inputFileStream))
bufferedReader.use {
bufferedReader.forEachLine {
myContent.append(it)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
return myContent.toString()
}
}
我们重新运行程序,重新输入一段文本:
连续按下Back键退出程序,然后通过Device File Explore查看我们的data文件:
从桌面重新打开程序,发现我们之前输入的内容已经恢复了:
我们将数据恢复的操作放在了onCreate()方法中。首先我们判断文件中的内容是否为空,若不为空就将文件中的内容恢复到输入框中,然后调用了EditText.setSelection()方法,并传入文本的长度来将光标移动到输入框末尾以便继续输入。
三、SharePreferences存储
不同于文件存储的方式,SharePreferences是以键值对的方式来存储数据的。也就是说,当我们保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。并且,SharePreferences还支持多种不同数据类型的存储,如果存储的数据类型是Int类型,那么读取出来数据也是Int类型。如果存储的数据是String类型,那么读取出来的数据仍然是String类型。
需要注意的是:
SharedPreferences对象本身只能读取数据,而不能存储和修改数据。如果要实现数据的存储和修改,则需要通过Editor对象。具体操作过程是,首先通过SharedPreferences的编辑器edit()方法获取Editor对象,然后通过Editor对象存储key-value键值对数据,最后通过commit()方法提交数据。
3.1 获取SharePreferences对象
Android主要提供了以下两种方法用于得到SharePreferences对象:
- Context.getSharedPreferences()方法
getSharedPreferences()方法接收2个参数:① 指定SharePreferences文件名 ② 操作模式。SharePreferences文件默认都是存放在 /data/data/<包名>/shared_prefs/目录下的。操作模式只有MODE_PRIVATE,表示只有当前应用程序才可以对这个SharePreferences文件进行读写,也可以在参数位输入0代替。 - Activity.getPreferences()方法
getPreferences()方法只接收1个操作模式作为参数,使用这个方法会自动以当前Activity的类名作为SharePreferences的文件名
3.2 将数据存储到SharePreferences中
在得到了SharePreferences对象后,若想将数据存储到SharePreferences文件中,需要以下步骤:
- 调用SharePreferences对象的edit()方法获取一个SharePreferences.Editor对象
- 向Editor对象中添加数据(putBoolean()、putString等)
- 通过apply( )方法提交所添加的数据,完成数据的存储。
接下来,我们通过一个例子来将数据存储到SharePreferences中。创建一个SharedPreferencesTest项目,修改main_activity.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:layout_marginTop="10dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="SharePreferences文件的写入与读取" />
<Button
android:id="@+id/savaButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="存储数据" />
<Button
android:id="@+id/loadButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="读取数据" />
</LinearLayout>
我们为存储数据按钮设置点击事件,当点击存储按钮后将数据存入SharePreferences文件中。修改MainActivity.kt代码如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
为
savaButton.setOnClickListener {
//通过getSharedPreferences().edit()方法获得Editor对象
val myEditor = getSharedPreferences("shared_prefs_data", MODE_PRIVATE).edit()
//以键值对的方式向Editor对象中添加数据
myEditor.putString("name", "CodingBear")
myEditor.putInt("age", 18)
myEditor.putBoolean("single", true)
//将数据存储到SharePreferences文件中
myEditor.apply()
}
loadButton.setOnClickListener {
// to do
}
}
}
运行程序后我们发现,此时还没有生成shared_prefs/shared_prefs_data文件。
点击主界面的存储数据按钮后你会发现已经成功生成了shared_prefs_data.xml文件:
双击打开文件你会发现我们的数据是通过键值对的方式存储的:
细心的你可能会发现SharePreferences文件是使用XML格式来管理数据的,这跟之前使用文件流将数据存储到文件的方式是不同的!
3.3 从SharePreferences中读取数据
我们从SharePreferences文件中读取数据也十简单。之前我们是通过putXXX( )的方式将数据存储到SharedPreferences文件中的,那么读取数据就是通过相应的getXXX( )的方法进行的。不过这些getXXX( )方法需要接收两个参数:①键②默认值。当传入的键找不到对应的值时,就会将默认值返回。前面我们有说到过:SharedPreferences对象本身只能读取数据,数据的存储和修改都是通过Editor对象来实现的。所以不要搞混了,在我们下面的示例代码中我们getSharedPreferences()方法并没有通过调用Edit()来创建一个Editor对象。
接下来我们编写读取数据按钮的点击逻辑,实现从SharePreferences中读取数据:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
savaButton.setOnClickListener {
· · ·
}
loadButton.setOnClickListener {
//通过getSharedPreferences()方法获得SharedPreferences对象(SharedPreferences对象本身只能读取数据)
val mySharedPref = getSharedPreferences("shared_prefs_data", MODE_PRIVATE)
//取出数据
val name = mySharedPref.getString("name", "null")
val age = mySharedPref.getInt("age", 0)
val single = mySharedPref.getBoolean("single", false)
//弹出Toast显示数据
Toast.makeText(
this,
"Name is :${name},age is :${age},single is :${single}",
Toast.LENGTH_SHORT
).show()
}
}
}
点击读取数据,可以看到我们已经成功将SharedPreferences文件中的数据读取出来了:
3.4 通过SharePreferences实现记住密码功能
接下来我们通过SharePreferences实现记住密码功能,首先我们回到BroadcastBestPractice项目,然后在activity_login.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">
<!--账号输入区域-->
· · ·
</LinearLayout>
<!--密码输入区域-->
<LinearLayout
· · ·
</LinearLayout>
<!--记住密码区域-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/myRememberPassword"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="记住密码"
android:textSize="18sp" />
</LinearLayout>
<!--登陆按钮-->
<Button
· · ·
android:textSize="18sp" />
</LinearLayout>
然后修改LoginActivity.kt中的代码:
class LoginActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
//SharedPreferences对象
val mySharedPreferences = getSharedPreferences("pwd_file", MODE_PRIVATE)
//之前是否已选中记住密码
val isRemember = mySharedPreferences.getBoolean("remember", false)
if (isRemember) {
//若已选中记住密码 将SharedPreferences文件中的账号和密码设置到EditText中
val account = mySharedPreferences.getString("account", "")
val password = mySharedPreferences.getString("password", "")
myAccountEdit.setText(account)
myPasswordEdit.setText(password)
//恢复复选框选中状态
myRememberPassword.isChecked = true
}
//登陆按钮点击逻辑
myLoginButton.setOnClickListener {
val account = myAccountEdit.text.toString()
val password = myPasswordEdit.text.toString()
if (account == "admin" && password == "123456") {
//Editor对象
val myEditor = mySharedPreferences.edit()
//若点击了记住密码
if (myRememberPassword.isChecked) {
//将复选框状态和数据保存到SharedPreferences文件中
myEditor.putBoolean("remember", true)
myEditor.putString("account", account)
myEditor.putString("password", password)
} else {
//清空SharePreferences文件中的数据
myEditor.clear()
}
//保存数据
myEditor.apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} else {
Toast.makeText(this, "账号或密码错误,清重新输入!", Toast.LENGTH_SHORT).show()
myPasswordEdit.setText("")
}
}
}
}
可以看到我们在点击登陆按钮后会先判断是否选中了复选框。若选中,则需要将选中状态、账号和密码都通过Editor对象保存在SharePreferences文件中。若未选中,则清空SharePreferences文件中的数据。
运行程序,输入帐号和密码点击记住密码后登陆,可以看到数据已经被保存到SharePreferences文件中了:
当我们点击强制下限广播按钮后会重新回到登录界面,并且账号和密码还有复选框的状态都恢复了:
四、SQLite数据库存储
SQLite是一款轻量级别的关系型数据库,它的运行速度非常快,占用资源非常少。
4.1 创建数据库
Android有一个SQLiteOpenHelper类来帮助我们管理SQLite数据库。SQLiteOpenHelper是一个抽象类,所以我们若想使用它就必须要创建一个自定义的Helper类去继承它并重写onCreate()和onUpgrade()抽象方法。我们在重写的onCreate() 方法中实现创建数据库的逻辑,在onUpgrade() 方法中实现升级数据库的逻辑。
1.抽象类中的方法并不一定都是抽象方法,一个抽象类可以有0或多个抽象方法。
2.抽象类中的抽象方法只有方法的声明,没有具体的实现。
3.当一个类继承抽象类时,它必须实现抽象类中的所有抽象方法。
SQLiteOpenHelper抽象类中有两个非常重要的实例方法:getReadableDatabase() 和getWritableDatabase() 。这两个方法都可以创建或者打开一个数据库(若数据库已存在则直接打开,否则会创建一个新的数据库)。并且,这两个方法会返回一个可对数据库进行读写操作的SQLiteDatabase对象。不同的是,当数据库不可写入时(如磁盘已满),getReadableDatabase()方法返回的对象将以只读的方式打开数据库,而getWritableDatabase()方法则将出现异常。
SQLiteOpenHelper抽象类一般重写参数较少的构造方法,该构造方法要求传入4个参数:①Context②数据库名称③传入null即可④数据库版本号。我们在构建出SQLiteOpenHelper对象后,再调用它的getReadableDatabase()或getWritableDatabase()方法就能够创建数据库了。数据库的文件都会存放在/data/data/<包名>/databases/目录下。
看了这么多文字一定很难受,下面我们就通过一个例子来体会一下。首先我们创建一个DatabaseTest项目,我们希望创建一个名为BookStore.db的数据库,然后在这个数据库中新建一个Book表。表中有id(主键)、作者、价格、页数和书名等列。它的创建SQL语句如下:
CREATE TABLE Book(
列 类型 主键 递增
id integer primary key autoincrement,
author text,
price real,
pages integer,
name text
);
这条SQL语句将创建一个名为"Book"的表,其中包含五列:“id”、“author”、“price”、“pages"和"name”。"id"列是主键,每次新记录插入表时,会自动递增。"author"和"name"列是文本类型,"price"列是实数类型(可以存储浮点数),"pages"列是整数类型。我们需要在代码中执行SQL语句才能完成创建表的操作,我们在DatabaseTest项目中新建一个MyDatabaseHelper类,并让其继承自SQLiteOpenHelper:
/**
* 管理数据库的Helper类
*/
上下文 数据库名 数据库版本号
class MyDatabaseHelper(val context: Context, name: String, version: Int): SQLiteOpenHelper(context, name, null, version) {
//Book建表语句字符串
private val createBook = "create table Book (" +
"id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)"
//创建数据库时调用
override fun onCreate(database: SQLiteDatabase?) {
//执行Book建表SQL语句
database?.execSQL(createBook)
Toast.makeText(context, "数据库创建成功!", Toast.LENGTH_SHORT).show()
}
override fun onUpgrade(database: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
}
}
可以看到我们将创建Book表的SQL语句定义成了一个字符串变量createBook,然后在onCreate()方法中通过execSQL()方法来执行这个SQL建表语句。然后我们修改activity_main.xml中的代码:
<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/createDatabaseButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="创建数据库" />
<Button
android:id="@+id/addDataButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="添加数据" />
<Button
android:id="@+id/updateDataButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="更新数据" />
<Button
android:id="@+id/deleteDataButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="删除数据" />
<Button
android:id="@+id/queryDataButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="查询数据" />
<Button
android:id="@+id/replaceDataButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="替换数据" />
</LinearLayout>
我们在主界面添加了许多Button,我们先从“创建数据库”这个按钮开始实现吧!当我们点击“创建数据库”这个Button的时候会通过MyDatabaseHelper创建数据库。MainActivity.kt的代码如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//创建MyDatabaseHelper对象 通过构造函数指定数据库名和版本号
val dataBaseHelper = MyDatabaseHelper(this, "BookStore", 1)
/*创建数据库按钮点击事件*/
createDatabaseButton.setOnClickListener {
//通过getWritableDatabase()方法创建数据库
dataBaseHelper.writableDatabase
}
}
}
我们先创建了自定的MyDatabaseHelper类对象,然后通过往构造方法中传入参数来指定数据库名和版本号。在按钮的点击事件中,我们通过getWritableDatabase()方法来创建数据库。运行程序,当我们第一次点击主界面的“创建数据库”按钮时会创建BookStore.db数据库并弹出一个Toast。当我们再次点击时并不会弹出Toast弹窗,这是因为已经存在BookStore.db这个数据库了,因此不会再创建一次。可以看到我们已经成功生成了数据库文件:
我们可以右击BookStore点击Save As将它保存到我们本地任意一个位置。然后在Android Studio左上角借助DB Browser插件帮我们打开BookStore.db数据库文件:
选择数据库文件后点击OK:
可以看到我们的数据库表确实创建成功了:
4.2 升级数据库
在SQLiteOpenHelper类中还有一个onUpgrade()方法,当数据库升级(新版本号大于旧版本号)时该方法会触发。该方法用于对现有的数据库进行升级,当我们对数据库的结构进行更改或者添加新的表时,你应该通过升级数据库的方式来更新数据库。目前,我们项目中已经存在一个Book表用于存放书籍的详细信息,如果我们想再往数据库中添加一个Category表该怎么做呢,这就需要通过更新数据库来完成。创建Category表的SQL语句如下:
CREATE TABLE Category(
列 类型 主 键 递增
id integer primary key autoincrement,
category_name text,
category_code integer,
);
我们在MyDatabaseHelper中添加这个建表语句SQL字符串:
class MyDatabaseHelper(val context: Context, name: String, version: Int): SQLiteOpenHelper(context, name, null, version) {
private val createBook = · · ·
//Category建表语句字符串
private val createCategory = "create table Category (" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"
//创建数据库时调用
override fun onCreate(database: SQLiteDatabase?) {
database?.execSQL(createBook)
//执行Category建表语句
database?.execSQL(createCategory)
Toast.makeText(context, "数据库创建成功!", Toast.LENGTH_SHORT).show()
}
//升级数据库(新版本号大于旧版本号会执行)
override fun onUpgrade(database: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
· · ·
}
}
这次重新运行程序,不论你点击多少次“创建数据库”的Button都不会弹出Toast,也不会在BookStore.db中新增加Category表。这是因为此时BookStore.db数据库已经存在了,onCreate()方法不会再次执行,因此添加表的操作也就无法得到执行了。我们也可以卸载然后重新安装程序,这时BookStore数据库就已经不存在了。当我们再次点击“创建数据库”按钮时就会重新执行onCreate()方法来创建数据库,Category表也就创建成功了。
卸载重装程序就为了更新数据库简直是太极端了,我们可以巧妙的运用SQLiteOpenHelper的onUpgrade()升级方法来解决这个问题。修改MyDatabaseHelper:
/**
* 管理数据库的Helper类
*/
class MyDatabaseHelper(val context: Context, name: String, version: Int): SQLiteOpenHelper(context, name, null, version) {
private val createBook = · · ·
//Category建表语句字符串
private val createCategory = · · ·
//创建数据库
override fun onCreate(database: SQLiteDatabase?) {
· · ·
database?.execSQL(createCategory)
· · ·
}
//升级数据库(新版本号大于旧版本号会执行)
override fun onUpgrade(database: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
//若Book和Category表存在则删除
database?.execSQL("drop table if exists Book")
database?.execSQL("drop table if exists Category")
//重新执行建表语句创建数据库
onCreate(database)
}
}
可以看到我们在onUpgrade()方法中执行了两条SQL语句,若Book表和Category表存在,则删除它们并调用onCreate()方法重新创建数据库。最后一个问题就是怎么才能让onUpgrade()方法执行呢,我们知道onUpgrade()方法会在数据库新版本号大于旧版本号时执行。所以我们在MainActivity中构建数据库时传入一个比先前数据库版本大的数字就可以让onUpgrade()方法得到执行了:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
· · ·
//创建MyDatabaseHelper对象 通过构造函数指定数据库名和版本号
val dataBaseHelper = MyDatabaseHelper(this, "BookStore", 2)
· · ·
}
这里我们将数据库版本号指定为2,重新运行程序并点击“创建数据库”Button就会再次弹出Toast弹窗了。可以看到我们的数据库中确实增加了一个Category表:
当然你也可以通过let函数对上述代码进行简化:
database?.execSQL("drop table if exists Book")
database?.execSQL("drop table if exists Category")
|
|
V
database?.let {
it.execSQL("drop table if exists Book")
it.execSQL("drop table if exists Category")
}
4.3 添加数据
数据库中对数据进行的操作无非就4种,即CRUD。C代表添加(Create)、R代表查询(Retrieve)、U代表更新(Update)、D代表删除(Delete)。在SQL语言中添加数据使用insert、查询数据使用select、更新数据使用update、删除数据使用delete。
我们通过SQLiteOpenHelper的getReadableDatabase()和getWritableDatabase()方法可以得到一个SQLiteDatabase对象,借助这个对象就可以进行CRUD操作了。
SQLiteDatabase提供了一个insert()方法用来添加数据,insert()方法接收3个参数:①表名,指定需要将数据插入到哪张表中 ②一般传入null即可 ③ContentValues对象,它提供put()方法,用于向ContentValues中添加数据。
现在,我们实现“添加数据”按钮的点击逻辑,修改MainActivity中的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dataBaseHelper = MyDatabaseHelper(this, "BookStore", 2)
createDatabaseButton.setOnClickListener {
· · ·
}
/*添加数据按钮点击*/
addDataButton.setOnClickListener {
//SQLiteDatabase对象(借助它进行CRUD)
val dataBase = dataBaseHelper.writableDatabase
//要添加的第一条数据
val values1 = ContentValues().apply {
//往ContentValues对象中添加数据
put("name", "The Da Vinci Code")
put("author", "Dan Brown")
put("pages", 454)
put("price", 16.69)
}
//要添加的第二条数据
val values2 = ContentValues().apply {
//往ContentValues对象中添加数据
put("name", "The Lost Symbol")
put("author", "Dan Brown")
put("pages", 510)
put("price", 19.95)
}
//向数据库中插入数据
dataBase?.let {
//插入第一条数据
it.insert("Book", null, values1)
//插入第二条数据
it.insert("Book", null, values2)
}
}
}
}
再次运行程序,点击“添加数据”按钮。可以看到,我们已经成功向Book表中插入了两条数据:
4.4 更新数据
SQLiteDatabase中提供了一个update() 方法用于更新数据库中的数据。它有4个参数,分别是:
- 表名:要更新数据的表名称。
- ContentValues:一个ContentValues对象,包含了要更新的数据。
- WHERE语句:一个可选的WHERE子句,用于指定更新哪些行。如果不指定WHERE子句,则会更新表中的所有行。
- WHERE语句的数组:一个可选的参数数组,用于替换WHERE语句中的?占位符。如果WHERE语句参数中包含占位符(?),则可以通过这个数组提供相应的参数值。
我们修改MainActivity.kt中的代码,给“更新数据”按钮增加点击逻辑:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//创建MyDatabaseHelper对象 通过构造函数指定数据库名和版本号
val dataBaseHelper = MyDatabaseHelper(this, "BookStore", 2)
/*创建数据库按钮点击*/
createDatabaseButton.setOnClickListener {
· · ·
}
/*添加数据按钮点击*/
addDataButton.setOnClickListener {
· · ·
}
/*更新数据按钮点击*/
updateDataButton.setOnClickListener {
//SQLiteDatabase对象
val dataBase = dataBaseHelper.writableDatabase
//ContentValues用于存储要更新的数据
val values = ContentValues()
//新的price数据
values.put("price", 10.99)
//在Book表中更新name=The Da Vinci Code这一行的数据 将price改为10.99
dataBase.update("Book", values, "name=?", arrayOf("The Da Vinci Code"))
}
}
}
可以看到我们首先创建了一个SQLiteDatabase对象,然后将要更新的数据price存储到ContentValues对象中。然后调用SQLiteDatabase对象的update()方法,首先传入要更新数据的表名Book,然后传入包含新数据的ContentValues对象。第三个参数对应的是WHERE语句,我们传入name=?表示我们要修改name=?的这一行数据。而?是一个占位符号,我们在第四个参数传入一个字符串数组作为占为符?的内容。
现在运行程序,点击“更新数据”按钮,可以看到达芬奇密码这本书的价格已经修改了:
4.5 删除数据
SQLiteDatabase中提供了一个delete() 方法用于删除数据库中的数据。它接收3个参数:
- 表名:要删除数据的表名称。
- WHERE语句:一个可选的WHERE子句,用于指定删除哪些行。如果不指定WHERE子句,则会删除表中的所有数据。
- WHERE语句的数组:一个可选的参数数组,用于替换WHERE语句中的?占位符。如果WHERE语句参数中包含占位符(?),则可以通过这个数组提供相应的参数值。
我们修改MainActivity.kt中的代码,给“删除数据”按钮增加点击逻辑:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//创建MyDatabaseHelper对象 通过构造函数指定数据库名和版本号
val dataBaseHelper = MyDatabaseHelper(this, "BookStore", 2)
/*创建数据库按钮点击*/
createDatabaseButton.setOnClickListener {
· · ·
}
/*添加数据按钮点击*/
addDataButton.setOnClickListener {
· · ·
}
/*更新数据按钮点击*/
updateDataButton.setOnClickListener {
· · ·
}
/*删除数据按钮点击*/
deleteDataButton.setOnClickListener {
//SQLiteDatabase对象
val database = dataBaseHelper.writableDatabase
//删除Book表中 pages大于500的数据
database.delete("Book", "pages>?", arrayOf("500"))
}
}
}
可以看到,当我们点击“删除数据”按钮后,数据库中页数大于500页的书籍信息就被删掉了:
4.6 查询数据
SQLiteDatabase中提供了一个query() 方法用于查询数据库中的数据,它接收的参数非常复杂,最短的一个方法重载也需要传入7个参数。,我们就简单看一下这七个参数的含义吧:
- 表名:要查询的表的名称。
- 列名:要查询的列的名称。可以是单个列的名称,也可以是多个列的名称,以逗号分隔。如果要查询所有列,可以将此参数设置为 null。如果不指定,则查询所有列。
- WHERE语句:查询条件,用于约束查询某一行或者某几行数据。如果不指定WHERE子句,则会查询表中的所有数据。
- WHERE语句的数组:一个可选的参数数组,用于替换WHERE语句中的?占位符。如果WHERE语句参数中包含占位符(?),则可以通过这个数组提供相应的参数值。
- GROUPBY:用于对查询结果进行分组的列名。如果不需要分组,可以将此参数设置为 null。
- HAVING:用于对GROUPBY分组后的结果进行过滤的条件。如果不需要过滤,可以将此参数设置为 null。
- ORDERBY:用于对查询结果进行排序的列名。如果不需要排序,可以将此参数设置为 null。
虽然query()方法的参数非常多,但是多数情况下我们只需要使用少数几个参数就可以完成查询操作。调用query()方法后会返回一个Cursor对象,查询到的所有数据都将从Cursor对象中取出。
在使用完Cursor后将Cursor关闭是十分重要的。关闭Cursor对象的主要原因是释放系统资源,包括内存和数据库连接等。如果不及时释放,可能会导致系统资源浪费、内存泄漏等问题。
我们来为查询数据按钮添加点击逻辑,修改MainActivity中的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//创建MyDatabaseHelper对象 通过构造函数指定数据库名和版本号
val dataBaseHelper = MyDatabaseHelper(this, "BookStore", 2)
/*创建数据库按钮点击*/
createDatabaseButton.setOnClickListener {
· · ·
}
/*添加数据按钮点击*/
addDataButton.setOnClickListener {
· · ·
}
/*更新数据按钮点击*/
updateDataButton.setOnClickListener {
· · ·
}
/*删除数据按钮点击*/
deleteDataButton.setOnClickListener {
· · ·
}
/*查询数据按钮点击*/
queryDataButton.setOnClickListener {
//SQLiteDatabase对象
val database = dataBaseHelper.writableDatabase
//query()方法返回一个Cursor对象
val cursor = database.query("Book", null, null, null, null, null, null)
//将游标(Cursor)移动到查询结果的第一行
//通常我们会使用这个方法来判断查询是否有结果,如果查询没有结果,这个方法会返回false。
if (cursor.moveToFirst()) {
do {
// 遍历Cursor对象,取出数据并打印
val name = cursor.getString(cursor.getColumnIndex("name"))
val author = cursor.getString(cursor.getColumnIndex("author"))
val pages = cursor.getInt(cursor.getColumnIndex("pages"))
val price = cursor.getDouble(cursor.getColumnIndex("price"))
Log.d("MainActivityTAG", "Book name is $name")
Log.d("MainActivityTAG", "Book author is $author")
Log.d("MainActivityTAG", "Book pages is $pages")
Log.d("MainActivityTAG", "Book price is $price")
} while (cursor.moveToNext())//判断下一行是否还有数据
}
//关闭Cursor 释放系统资源 避免内存泄漏
cursor.close()
}
}
}
通过Cursor.moveToNext()方法可以判断下一行是否还有数据。如果下一行还有数据则返回true,若已经处于最后一行则返回false。通过moveToNext()方法遍历query()方法返回的结果(Cursor对象),我们逐行取出Cursor对象中的数据并打印出来。可以看到我们将Book表中仅有的一条数据查询出来了:
当然你可以写卸载程序程序后重新运行,然后重新点击上面的按钮,切记不要点击删除数据按钮。然后再点击查询数据按钮:
可以看到我们将未删除数据前Book表中的数据查询出来了。
4.7 insert()、delete()和update()方法的返回值
其实SQLiteDatabase的insert()、delete()和update( )方法都是有返回值的!当数据添加/删除/更新成功后,相应的方法会返回一个Int类型的值,用来表示更新操作影响的行数。如果更新失败或没有行被更新,则返回0。
- insert() 方法的返回值表示新插入数据行的行数。
- delete() 方法的返回值表示从数据库中删除的行数。
- update() 方法的返回值是数据库中受影响的行数。
拿update()举例,我们将查询数据按钮的代码段稍微改造一下,将"dataBase.update()"方法的返回值保存起来,然后通过Toast弹窗显示出来:
class MainActivity : AppCompatActivity() {
· · ·
updateDataButton.setOnClickListener {
//SQLiteDatabase对象
val dataBase = dataBaseHelper.writableDatabase
//ContentValues用于存储要更新的数据
val values = ContentValues()
//新的price数据
values.put("price", 10.99)
//在Book表中更新name=The Da Vinci Code这一行的数据 将price改为10.99
var column_count = dataBase.update("Book", values, "name=?", arrayOf("The Da Vinci Code"))
Toast.makeText(this,"update:$column_count",Toast.LENGTH_SHORT).show()
}
· · ·
}
可以看到当我们点击"更新数据"按钮后,将达芬奇密码这本书的价格改成了10.99元。也就是说,update()方法只影响了达芬奇密码这一行数据,所以update()方法的返回值是1。
五、使用SQL操作数据库
前面我们都是通过Android提供的insert()、update()、delete()、query()辅助方法来对数据库中的数据进行操作的,当然你也可以使用SQL来操作数据库。
//SQLiteDatabase对象
val database = dataBaseHelper.writableDatabase
添加数据:
database.execSQL(
"insert into Book(name,author,pages,price) values(?,?,?,?)",
arrayOf("The Da Vinci Code", "Dan Brown", "454", "16.96")
)
更新数据:
database.execSQL(
"update Book set price = ? where name = ?",
arrayOf("10.99", "The Da Vinci Code")
)
删除数据:
database.execSQL("delete from Book where pages > ?", arrayOf("500"))
查询数据:
val cursor = database.rawQuery("select * from Book", null)
六、学会使用事物
事务是一个对数据库执行操作的工作单元,它以逻辑顺序完成一系列操作。在实际操作中,可以将多个SQLite查询组合成一组,然后作为事务的一部分进行执行,以确保数据的完整性和处理数据库错误。事务的特性可以保证让一系列的操作要么全部完成,要么一个都不会完成。
设想有这么个需求,DatabaseTest项目Book表中的数据已经失效了,现在需要将Book表中的旧数据全部替换成新数据。我们可以先使用delete()方法将Book表中的数据删除,然后再使用insert()方法将新数据添加到表中。我们要保证删除旧数据和添加新数据的操作必须一起完成,否则就要继续保留原来的旧数据。为“替换数据”按钮添加点击逻辑,修改MainActivity中的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//创建MyDatabaseHelper对象 通过构造函数指定数据库名和版本号
val dataBaseHelper = MyDatabaseHelper(this, "BookStore", 2)
/*创建数据库按钮点击*/
createDatabaseButton.setOnClickListener {
· · ·
}
/*添加数据按钮点击*/
addDataButton.setOnClickListener {
· · ·
}
/*更新数据按钮点击*/
updateDataButton.setOnClickListener {
· · ·
}
/*删除数据按钮点击*/
deleteDataButton.setOnClickListener {
· · ·
}
/*查询数据按钮点击*/
queryDataButton.setOnClickListener {
· · ·
}
/*替换数据按钮点击*/
replaceDataButton.setOnClickListener {
val database = dataBaseHelper.writableDatabase
//开启事务
database.beginTransaction()
try {
//删除Book表中全部数据
database.delete("Book", null, null)
//要添加的数据
val values = ContentValues().apply {
put("name", "Game of Thrones")
put("author", "George Martin")
put("pages", 720)
put("price", 20.85)
}
//向Book表中插入数据
database.insert("Book", null, values)
//设置事务执行成功状态
database.setTransactionSuccessful()
Toast.makeText(this, "ReplaceData Success!", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(this, "ReplaceData Error!", Toast.LENGTH_SHORT).show()
} finally {
//不论结果如何 关闭事务
database.endTransaction()
}
}
}
}
替换前的Book表:
可以看到我们已经成功将Book表中的数据全部替换了:
如果我们在try语句块内“删除Book表数据之后,添加数据之前”手动抛出异常,则Book表中的数据将不会替换。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//创建MyDatabaseHelper对象 通过构造函数指定数据库名和版本号
val dataBaseHelper = MyDatabaseHelper(this, "BookStore", 2)
/*创建数据库按钮点击*/
createDatabaseButton.setOnClickListener {
· · ·
}
/*添加数据按钮点击*/
addDataButton.setOnClickListener {
· · ·
}
/*更新数据按钮点击*/
updateDataButton.setOnClickListener {
· · ·
}
/*删除数据按钮点击*/
deleteDataButton.setOnClickListener {
· · ·
}
/*查询数据按钮点击*/
queryDataButton.setOnClickListener {
· · ·
}
/*替换数据按钮点击*/
replaceDataButton.setOnClickListener {
val database = dataBaseHelper.writableDatabase
database.beginTransaction()
try {
database.delete("Book", null, null)
//手动抛出异常 让事务失败
if (true) {
throw NullPointerException()
}
· · ·
database.insert("Book", null, values)
database.setTransactionSuccessful()
· · ·
}
}
这时虽然我们已经执行了Book表删除语句:
database.delete("Book", null, null)
但是由于事务的存在,只要我们的代码在“设置事务执行成功状态”这一行前发生异常:
//设置事务执行成功状态
database.setTransactionSuccessful()
当前事物就会失败,此时旧数据是删除不掉的。但是如果在“设置事务执行成功状态”这一行后发生异常,则该事物还是会成功的。所以在事务的逻辑中,一定要确保所有操作都成功后再通过setTransactionSuccessful()方法设置事务状态!