【18】应用开发——数据存储与持久化技术

提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方,欢迎各位在评论中指出。

一、数据持久化技术

我们把那些保存在内存中的数据称作是瞬时状态数据,而保存在存储设备中的数据则称为持久状态数据。由于瞬时数据很可能因为程序关闭或者内存被回收而导致数据丢失。为了解决这个问题,我们需要借助数据持久化技术将瞬时数据保存在存储设备中。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对象:

  1. Context.getSharedPreferences()方法
    getSharedPreferences()方法接收2个参数:① 指定SharePreferences文件名操作模式。SharePreferences文件默认都是存放在 /data/data/<包名>/shared_prefs/目录下的。操作模式只有MODE_PRIVATE,表示只有当前应用程序才可以对这个SharePreferences文件进行读写,也可以在参数位输入0代替。
  2. Activity.getPreferences()方法
    getPreferences()方法只接收1个操作模式作为参数,使用这个方法会自动以当前Activity的类名作为SharePreferences的文件名

3.2 将数据存储到SharePreferences中

在得到了SharePreferences对象后,若想将数据存储到SharePreferences文件中,需要以下步骤:

  1. 调用SharePreferences对象的edit()方法获取一个SharePreferences.Editor对象
  2. Editor对象中添加数据(putBoolean()、putString等)
  3. 通过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个参数。,我们就简单看一下这七个参数的含义吧:

  1. 表名:要查询的表的名称。
  2. 列名:要查询的列的名称。可以是单个列的名称,也可以是多个列的名称,以逗号分隔。如果要查询所有列,可以将此参数设置为 null。如果不指定,则查询所有列。
  3. WHERE语句:查询条件,用于约束查询某一行或者某几行数据。如果不指定WHERE子句,则会查询表中的所有数据。
  4. WHERE语句的数组:一个可选的参数数组,用于替换WHERE语句中的?占位符。如果WHERE语句参数中包含占位符(?),则可以通过这个数组提供相应的参数值。
  5. GROUPBY:用于对查询结果进行分组的列名。如果不需要分组,可以将此参数设置为 null。
  6. HAVING:用于对GROUPBY分组后的结果进行过滤的条件。如果不需要过滤,可以将此参数设置为 null。
  7. 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()方法设置事务状态

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值