8月2日 第6章 数据存储全方案——详解持久化技术

数据存储目前主要有

文件存储、SharedPreferences 存储以及数据库存储

1.文件

文件存储就是新建一个文件以字符串的形式存储数据

不实际上书中并没有提到是否只能以字符串形式存储

书中在这部分的原文是:

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

因此是我先入为主的认为只能存储字符串形式,我并不知道这是否是正确的,目前只是假定如此。

1.存储

文件存储就是新建一个文件以字符串的形式存储数据

不实际上书中并没有提到是否只能以字符串形式存储

书中在这部分的原文是:

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

因此是我先入为主的认为只能存储字符串形式,我并不知道这是否是正确的,目前只是假定如此。

现在看看如何写入文件吧:

public void save(String inputText){
    FileOutputStream out = null;
    BufferedWriter writer = null;
    try {
        out = openFileOutput("data1", Context.MODE_PRIVATE);
        writer = new BufferedWriter(new OutputStreamWriter(out));
        writer.write(inputText);

    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if (writer != null){
                writer.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

这里是通过 openFileOutput()得到一个 FileOutputStream 对象,然后再使用它来构建出一个 OutputStreamWriter 对象,继续使用 OutputStreamWriter 构建出一个 BufferedWriter 对象,然后通过 BufferedWriter 将文本内容写入到文件中。

现在写一个实例来实现这个功能

我并没有按照书上说的来写,我尝试使用了约束布局来进行书写布局

我加入了一个EditText来进行用户输入,并且创建了一个Button来进行保存,而不是像书中那样使用生命周期中的 onDestroy()方法(销毁),这样会方便许多

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/edit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="写点东西在这里"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="保存"

        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/edit"
        tools:ignore="InvalidId,MissingConstraints" />

  

</androidx.constraintlayout.widget.ConstraintLayout>

然后我们再对MainActivity进行编辑

private EditText edit;
//这个我并不知道是什么,是Android studio要求加上的,经过搜索,是用来无视膨胀警告的,为什么会发生这个警告我并不清楚
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    EdgeToEdge.enable(this);
    setContentView(R.layout.activity_main);
    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
        Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
        v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
        return insets;
    });
    //书中使用了强转,但实际不需要强转,可能是因为版本不一样?
    edit = findViewById(R.id.edit);
    Button button = findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            String inputText = edit.getText().toString();
            save(inputText);
            Toast.makeText(MainActivity.this,"已保存至data1文件",Toast.LENGTH_SHORT).show();
        }
    });
}

public void save(String inputText){
    FileOutputStream out = null;
    BufferedWriter writer = null;
    try {
        out = openFileOutput("data1", Context.MODE_PRIVATE);
        writer = new BufferedWriter(new OutputStreamWriter(out));
        writer.write(inputText);

    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if (writer != null){
                writer.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

这里我使用了按钮来进行save方法的调用,同时使用Toast来显示文字表示成功执行

运行后我查看书中说就会存在到/data/data/com.example.filepersistencetest/files/路径下

由于书中使用的是模拟器,而我并没能成功创建模拟器,因此我使用的是iqoo pro实机,另外,它的Android版本是11

因此我没有看书中查找文件的方式,直接实机操作打开文件管理冲进Android/data/文件,无论使用系统自带文件管理还是mt文件管理器都无法找到com.example.filepersistencetest文件夹。我第一反应是文件没有成功生成,从而开始检查代码。这浪费了我很多时间,因此在这里提一嘴。

但实际上是生成了的,请放心

后来经过了一番无用的努力,我尝试使用书中的方法,但我并没有搜索到一个叫做Android Device Monitor的工具(Ctrl+Shift+A打开搜索输入Device File Explorer,注:这是第三版才有的内容,我正在学习的第二版并没有,我是查阅的第三版才得到的方法)

但是很遗憾,我还是找到了这个工具

我是在Device Manager功能下选择对应设备最后的三个点发现了一个叫做Open in Device Explorer,哦是的,这就是我在找的文件路径

打开它,与书中所描述的文件路径一模一样/data/data/com.example.filepersistencetest/files/

在这个目录下我发现了我创建的data1文件,他并没有后缀名。

2.读取

文件读取的代码与文件存储的代码大差不差:

public String load(){
    FileInputStream in = null;
    BufferedReader reader = null;
    StringBuilder content = new StringBuilder();
    try {
        in = openFileInput("data1");
        reader = new BufferedReader(new InputStreamReader(in));
        String line = "";
        while ((line = reader.readLine()) != null){
            content.append(line);
        }
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        if (reader != null){
            try {
                reader.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }
    return  content.toString();
}

这次是通过 openFileInput()方法获取到 FileInputStream 对象,然后使用它构建出了 InputStreamReader 对象,接着再构建出一个 BufferedReader 对象,这样就可以通过 BufferedReader 一行行地读取,然后把文件中所有的文本内容全部读取出来,并存放在 StringBuilder 对象中,最后将读取到的内容返回。

也只是将方法更换了而已,书写的结构基本没有变

那么现在,读取方法会返回一个字符串,因此我们需要设定一个字符串来接收它

书中是将字符串返回到EditText,我则是新建了一个TextView来接收,并且创建了第二个按钮用来读取代码如下

activity_main.xml

<TextView
    android:id="@+id/tv"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="0000000"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/button"
    tools:ignore="MissingConstraints" />

<Button
    android:id="@+id/button2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="读取"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/tv" />

MainActivity.java

TextView tv = findViewById(R.id.tv);
Button button2 = findViewById(R.id.button2);
button2.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        tv.setText(load());
    }
});

这样运行后点击保存按钮在点击读取按钮就可以将当前输入框内的文字显示在TextView中

2.SharedPreferences

Android有三种方法来获取SharedPreferences对象

  1. Context 类中的 getSharedPreferences()方法

      此方法接收两个参数,第一个参数用于指定 SharedPreferences 文件的名称,如果指定的文件 不存在则会创建一个,SharedPreferences 文件都是存放在/data/data//shared_prefs/ 目录下的。第二个参数用于指定操作模式,目前只有 MODE_PRIVATE 这一种模式可选,它是默 认的操作模式,和直接传入 0 效果是相同的,表示只有当前的应用程序才可以对这个 SharedPreferences 文件进行读写。

  2. Activity 类中的 getPreferences()方法

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

  3. PreferenceManager 类中的 getDefaultSharedPreferences()方法

      这是一个静态方法,它接收一个 Context 参数,并自动使用当前应用程序的包名作为前缀 来命名 SharedPreferences 文件。得到了 SharedPreferences 对象之后,就可以开始向 SharedPreferences 文件中存储数据了,主要可以分为 3 步实现。

      (1) 调用 SharedPreferences 对象的 edit()方法来获取一个SharedPreferences.Editor 对象。

      (2) 向 SharedPreferences.Editor 对象中添加数据,比如添加一个布尔型数据就使用 putBoolean()方法,添加一个字符串则使用 putString()方法,以此类推。

      (3) 调用 apply()方法将添加的数据提交,从而完成数据存储操作。

1.SharedPreferences 存储

现在开始存储数据的操作

同样的使用约束布局创建一个Button

<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="存储"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

然后再MainActivity.java文件中编辑

Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        SharedPreferences.Editor editor = getSharedPreferences("data",
                MODE_PRIVATE).edit();
        editor.putString("name","Tom");
        editor.putInt("age",20);
        editor.putBoolean("married",false);
        editor.apply();
    }
});

是的这个按钮执行的便是存储操作

editor.putXXX便是存储方法

运行后点击按钮

现在使用相同的方法前往/data/data/com.example. sharedpreferencestest/shared_prefs/目录下

我们会发现一个data.xml文件内容是

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="name">Tom</string>
    <boolean name="married" value="false" />
    <int name="age" value="20" />
</map>

是的它是使用xml进行存储数据的

2.SharedPreferences 读取

我们在存储操作时使用的是putxxx,而读取更加简单,只需要将put改为get

get方法有两个参数,第一个是键就是存储时的"name"等等,第二个是默认值,是没有找到对应数据时返回的值

现在我们新建一个按钮用来触发读取,在创建一个TextView用来显示数据

<Button
    android:id="@+id/button2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="读取"
    app:layout_constraintBottom_toTopOf="@+id/button"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="TextView"
    app:layout_constraintBottom_toTopOf="@+id/button2"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

然后再MainActivity中写显示逻辑

Button button1 =findViewById(R.id.button2);
button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        SharedPreferences pref = getSharedPreferences("data",MODE_PRIVATE);
        String name = pref.getString("name","");
        int age = pref.getInt("age",0);
        boolean married = pref.getBoolean("married",false);
        TextView tv = findViewById(R.id.textView);
        tv.setText("姓名:"+ name + "年龄:" + age + "婚配:" + married);
    }
});

这样我们先点击存储按钮再点击读取按钮就会发现textview的显示发生了改变

3.SQLite 数据库

SQLite 是一款轻量级的关系型数据库

Android 正是把这个功能极为强大的数据库嵌入到了系统当中

适用于存储大量复杂的关系型数据

SQLiteOpenHelper 是一个抽象类有两个抽象方法,分别是 onCreate() 和 onUpgrade()

SQLiteOpenHelper 中还有两个非常重要的实例方法:getReadableDatabase()和 getWritableDatabase()。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。不同的是,当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而 getWritableDatabase()方法则将出现异常。

1.创建SQLite 数据库

首先呢由于SQLiteOpenHelper 是一个抽象类所以我们需要新建一个类来继承这个抽象类并重写抽象方法

新建 MyDatabaseHelper

package com.example.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;

import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {

    public static final String CREATE_BOOK = "create table Book ("
            +"id integer primary key autoincrement, "
            +"author text, "
            +"price real, "
            +"name text) ";

    private Context mContext;

    public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(CREATE_BOOK);
        Toast.makeText(mContext, "创建成功", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

    }
}

我们可以看到,创建数据库的SQL指令是以字符串的形式进行存储的

而在外面的onCreate方法也就是创建方法中,我们使用sqLiteDatabase.execSQL(CREATE_BOOK);方法来执行字符串形式的SQL指令,并发出Toast来提示已经创建成功.

我们来到activity_main.xml文件,添加一个button用来执行创建指令

<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="创建数据库"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

现在来修改MainActivity.java

MyDatabaseHelper dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,1);
Button button1 = findViewById(R.id.button);
button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        dbHelper.getWritableDatabase();
    }
});

我们可以看到,创建了一个我们刚刚创建的类MyDatabaseHelper对象,并传入了this,"BookStore.db",null,1这四个参数

其中this是Context

"BookStore.db"是数据库名

第三个参数允许我们在查询数据的时候返回一个自定义的 Cursor,一般都是传入 null

第四个参数 1 表示当前数据库的版本号

按下按钮则是调用了getWritableDatabase()方法

现在我们运行程序

点击一次按钮会弹出Toast提示

再次点击就没有了

这是因为我们的Toast写在数据库的onCreate(创建)方法

getWritableDatabase()找不到指定的数据库所以进行了创建,调用了onCreate方法

之后都能找到,就不会调用onCreate方法了

之后是查询数据库是否创建成功,这部分我进行了跳过

2.升级数据库

这部分非常简单在我们的MyDatabaseHelper类中有一个onUpgrade()的方法

当我们在MainActivity中使用的new MyDatabaseHelper()时将最后一个参数改为比之前的参数更大的值就会触发onUpgrade()方法

书中使用的例子是将1改为2,然后再onUpgrade()方法中删除原本的表并触发onCreate()方法来重新创建表代码如下

MyDatabaseHelper

package com.example.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;

import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {

    public static final String CREATE_BOOK = "create table Book ("
            +"id integer primary key autoincrement, "
            +"author text, "
            +"price real, "
            +"name text) ";
    public static final String CREATE_CATEGORY = "create table Category ("
            +"id integer primary key autoincrement, "
            +"category_name text, "
            +"category_code integer) ";

    private Context mContext;

    public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(CREATE_BOOK);
        sqLiteDatabase.execSQL(CREATE_CATEGORY);
        Toast.makeText(mContext, "创建成功", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
        sqLiteDatabase.execSQL("drop table if exists Book");
        sqLiteDatabase.execSQL("drop table if exists Category");
        onCreate(sqLiteDatabase);

    }
}

MainActivity只需要把1改为2 就行,这里就不放代码了

3.对数据库进行操作

我们可以对数据库进行添加C(Create),R 代 表查询(Retrieve),U 代表更新(Update),D 代表删除(Delete)这四个操作简称为CRUD

在 Android 中可以不编写 SQL 语句,也能完成以上CRUD

前面我们已经知道,调用 SQLiteOpenHelper 的 getReadableDatabase()或 getWritableDatabase()方法是可以用于创建和升级数据库的,而且这两个方法还都会返回一个 SQLiteDatabase 对象

我们可以使用他们返回的对象来进行CRUD

1.添加

SQLiteDatabase 中提供了一 个 insert()方法,这个方法就是专门用于添加数据的

第一个参数是表名

第二个是用于在未指定添加数据的情况下给某些可为空的列自动赋值 NULL,一般用不到,直接传入 null

第三个参数是一个 ContentValues 对象,它提供了一系列的 put()方法重载,用于向 ContentValues 中添加数据,需要传入将表中的每个列名以及相应的待添加数据

添加一个按钮用来触发添加数据

<Button
    android:id="@+id/button2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="添加数据"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/button" />

写按钮被按下的逻辑

Button button2 = findViewById(R.id.button2);
button2.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        //数据
        values.put("name", "The Da Vinci Code");
        values.put("author", "Dan Brown");
        values.put("price", 16.96);
        //插入数据
        db.insert("Book", null, values);
    }
});

先获取 SQLiteDatabase 对象,然后使用 ContentValues 来对数据进行组装。这里只对 Book 表里其中四列的数据进行了组装,id 那一列没给它赋值。因为在前面创建表的时候,我们将 id 列设置为自增长,它的值会在入库的时候自动生成。接下来调用 insert()方法将数据添加到表当中。

由于我前面创建表的时候没有创建页数,所以这里也比原文少了页数一条(懒得加就直接删了)

2.修改

SQLiteDatabase 中也提供了一个非常好用的 update()方法,用于对数据进行更新

第一个参数还是表名,在这里指定去更新哪张表里的数据。

第二个参数也还是 ContentValues 对象,要把更新数据在这里组装进去。

第三、第四个参数则是用于约束,更新某一行或某几行中的数据,不指定的话默认就是更新所有行

首先还是创建一个按钮

Button
    android:id="@+id/button3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:text="修改数据"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/button2" />

然后就是写逻辑

Button button3 = findViewById(R.id.button3);
button3.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put("price", 10.99);
        db.update("Book", values, "name = ?", new String[] { "The Da Vinci Code"});
    }
});

前面的就不说了

从第三行开始,我们只给 ContentValues了一个一组数据,只改动价格变成10.99

第四行是更新其中"name = ?"中的?是一个占位符,而紧随其后的new String[] { "The Da Vinci Code"}这个数组则是用来对第三个参数中的每一个?提供相应的内容

这里的意思是给Book表中的所有名字是The Da Vinci Code的价格改为10.99

3.删除

SQLiteDatabase 中提供了一个 delete()方法,专门用于删除数据

这个方法接收 3 个参数,

第一个参数仍然是表名,

第二、第三个参数还是用于约束删除某一行或某几行的数据,不指定的话默认就是删除所有行。

同样的创建按钮

<Button
    android:id="@+id/button4"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="删除数据"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/button3" />

按钮的逻辑

Button button4 = findViewById(R.id.button4);
button4.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        db.delete("Book", "price < ?", new String[] { "11" });
    }
});

由于上面添加的时候只添加了一个,所以这里我将删除的条件改为了price小于11

我们只有一本书并且在上次修改过后他的价格是10.99刚好小于11

所以删除后会将这本书删除

4.查询数据

终于到查询了,可以直观的验证我们以上的所有逻辑了

这里只会介绍 Android 上的查询功能,如果你对 SQL 语言非常感兴趣,可以找一本专门介绍 SQL 的书进行学习

SQLiteDatabase 中还提供了一个 query()方法用于对数据进行查询。

这个方法的参数非常复杂,最短的一个方法重载也需要传入 7 个参数

第一个参数不用说,当然还是表名。

第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。

第三、第四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据。

第五个参数用于指定需要去group by 的列,不指定则表示不对查询结果进行 group by 操作。

第六个参数用于对 group by 之后的数据进行进一步的过滤,不指定则表示不进行过滤。

第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式

好像很多但是没事,我们继续

我们还是新建一个按钮

<Button
    android:id="@+id/button5"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="12dp"
    android:text="查询"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/button4" />

然后是我们的逻辑

Button button5 = findViewById(R.id.button5);
button5.setOnClickListener(new View.OnClickListener() {
    @SuppressLint("Range")
    @Override
    public void onClick(View v) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        // 查询 Book 表中所有的数据
        Cursor cursor = db.query("Book", null, null, null, null, null, null);
        if (cursor.moveToFirst()) {
            do {
                // 遍历 Cursor 对象,取出数据并打印
                String name = cursor.getString(cursor.getColumnIndex("name"));
                String author = cursor.getString(cursor.getColumnIndex
                        ("author"));
                double price = cursor.getDouble(cursor.getColumnIndex
                        ("price"));
                Log.d("MainActivity", "book name is " + name);
                Log.d("MainActivity", "book author is " + author);
                Log.d("MainActivity", "book price is " + price);
            } while (cursor.moveToNext());
        }
        cursor.close();
    }
});

这里第一个参数指明去查询 Book 表,后面的参数全部为 null。这就表示希望查询这张表中的所有数据。

查询完之后就得到了一个 Cursor 对象,接着我们调用它的 moveToFirst()方法将数据的指针移动到第一行的位置(如果没有数据这个方法返回false),然后进入循环去遍历查询到的每一行数据。

在这个循环中可以通过 Cursor 的 getColumnIndex()方法获取到某一列在表中对应的位置索引,然后将这个索引传入到相应的取值方法中,就从数据库中读取到数据了

最后在使用log来打印

如果你希望在这里对之前的按钮进行测试,你可以选择将应用程序卸载,然后重新运行

也可以选择将前面说的版本号写一个更大的数,这样你就可以通过log测试之前的按钮是否成功。

这里是最开始设置的值与修改过后的值,删除后就没有数据了,所以不放图

5.使用SQL语句操作

添加数据的方法如下:

db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", new String[] { "The Da Vinci Code", "Dan Brown", "454", "16.96" });  
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", new String[] { "The Lost Symbol", "Dan Brown", "510", "19.95" });  

更新数据的方法如下:

 db.execSQL("update Book set price = ? where name = ?", new String[] { "10.99",  "The Da Vinci Code" });  

删除数据的方法如下:

db.execSQL("delete from Book where pages > ?", new String[] { "500" });  

查询数据的方法如下:

db.rawQuery("select * from Book", null);

除了查询数据的时候调用的是 SQLiteDatabase 的 rawQuery()方法,其他的操作 都是调用的 execSQL()方法

4.事务

此节来自第三版,第二版的LitePal似乎已被弃用,如今搜索github页面可以发现上次更新已是3年前

事务的使用场景:

比如你正在进行 一次转账操作,银行会先将转账的金额从你的账户中扣除,然后再向收款方的账户中添加等量 的金额。看上去好像没什么问题吧?可是,如果当你账户中的金额刚刚被扣除,这时由于一些 异常原因导致对方收款失败,这一部分钱就凭空消失了!当然银行肯定已经充分考虑到了这种情况,它会保证扣款和收款的操作要么一起成功,要么都不会成功

直接开始尝试吧

首先还是添加一个按钮

<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="替换"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/button5" />

按钮逻辑,我将书中的kotlin语言改为了java版本

Button button6 = findViewById(R.id.button);
        button6.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                db.beginTransaction();//开启事务
                try {
                    db.delete("Book", null, null);
//                    if (true) {
//                        // 手动抛出一个异常,让事务失败
//                        throw new NullPointerException();
//                    }
                    ContentValues values = new ContentValues();
                    values.put("name","游戏");
                    values.put("author","游戏1");
                    values.put("price","20.85");
                    db.insert("Book",null,values);
                    db.setTransactionSuccessful();//事务执行成功
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    db.endTransaction();//结束事务
                }
            }
        });

这里是调用SQLiteDatabase的 beginTransaction()方法开启一个事务

然后在捕捉异常代码块的最后调用setTransactionSuccessful()来表示事务成功运行了

最后在finally代码块中调用endTransaction()结束事务

而在删除数据的代码之后我们写了一个if代码来抛出一个异常,使事务执行失败

当if没被注释时点击替换后再查询还是老数据(删除数据代码未执行)

但if注释后,点击替换后变成了新数据

5.升级数据库的最佳写法

之前的升级数据库是删除后重新添加

这自然是不可以的,这会使原本的数据丢失

升级可以依靠数据库的版本号来进行升级代码示例如下

现在只需要创建一个表就可以了代码需求很简单

package com.example.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import androidx.annotation.Nullable;

public class MyDatabaseHelper1 extends SQLiteOpenHelper {
    private String createBook = "create table Book (" +
            " id integer primary key autoincrement," +
            "author text," +
            "price real," +
            "pages integer," +
            "name text)";

    private Context mContext;

    public MyDatabaseHelper1(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(createBook);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

    }
}

这次需要向数据库中再添加一张Category表。于是,修改 MyDatabaseHelper中的代码

package com.example.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import androidx.annotation.Nullable;

public class MyDatabaseHelper1 extends SQLiteOpenHelper {
    private String createBook = "create table Book (" +
            " id integer primary key autoincrement," +
            "author text," +
            "price real," +
            "pages integer," +
            "name text)";

    private String createCategory = "create table Category (" +
            "id integer primary key autoincrement," +
            "category_name text," +
            "category_code integer)";

    private Context mContext;

    public MyDatabaseHelper1(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(createBook);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
        if (i1 <= 1){
            sqLiteDatabase.execSQL(createCategory);
        }
    }
}

在onCreate()方法里我们新增了一条建表语句,然后又在onUpgrade()方法中添 加了一个if判断,如果用户数据库的旧版本号小于等于1,就只会创建一张Category表

这样当用户直接安装第2版的程序时,就会进入onCreate()方法,将两张表一起创建。而当用 户使用第2版的程序覆盖安装第1版的程序时,就会进入升级数据库的操作中,此时由于Book表已经存在了,因此只需要创建一张Category表即可

这次要给Book表和Category表之间建立关联,需要在Book 表中添加一个category_id字段

package com.example.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import androidx.annotation.Nullable;

public class MyDatabaseHelper1 extends SQLiteOpenHelper {
    private String createBook = "create table Book (" +
            " id integer primary key autoincrement," +
            "author text," +
            "price real," +
            "pages integer," +
            "name text," +
            "category_id integer)" ;

    private String createCategory = "create table Category (" +
            "id integer primary key autoincrement," +
            "category_name text," +
            "category_code integer)";

    private Context mContext;

    public MyDatabaseHelper1(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(createBook);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
        if (i1 <= 1){
            sqLiteDatabase.execSQL(createCategory);
        }
        if (i1 <= 2) {
            sqLiteDatabase.execSQL("alter table Book add column category_id integer");
        }
    }
}

我们在Book表的建表语句中添加了一个category_id列,这样当用户直接安装第3版的程序时,这个新增的列就会直接新建。但是如果有用户需要覆盖安装,就会进入升级数据库的操作。在onUpgrade()方法里,我们添加了一个条件,如果当前数据库的版本号是2,就会执行alter命令,为Book表新增一个category_id列。

每当升级一个数据库版本的时候,onUpgrade()方法里都一定要写一个相应的if判断语句。可以保证App在跨版本升级的时候, 每一次的数据库修改都能被全部执行。比如用户当前是从第2版升级到第3版,那么只有第二条判断语句会执行,而如果用户是直接从第1版升级到第3版,那么两条判断语句都会执行。使用这种方式来维护数据库的升级,不管版本怎样更新,都可以保证数据库的表结构是最新的,而且表中的数据完全不会丢失。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值