Android开发学习笔记(七) —— 详解数据存储全方案

一、什么是数据持久化

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

二、文件存储

2.1 概念

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

2.2 将数据存储到文件中

Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数,第一个参数是文件名,这里指定的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data/<package name>/fles/目录下的。第二个参数是文件的操作模式,主要有两种模式可选:

  • MODE_PRIVATE:默认操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖原文件中的内容。
  • MODE_APPEND:表示如果该文件已存在,就往文件里面追加内容,不存在就创建新文件。
  • MODE WORLD READABLEMODE WORLD WRITEABLE:这两种模式表示允许其他的应用程序对我们程序中的文件进行读写操作,不过由于这两种模式过于危险,很容易引起应用的安全性漏洞,已在Android4.2版本中被废弃。

实现小案例:活动销毁时,将输入框中的数据存储到文件中。

首先主活动的布局,一个输入框即可。在输入框中输入的数据只是瞬时数据,在活动被销毁后就会被回收。我们需要做的就是在数据被回收之前,将它存储到文件当中。

修改 MainActivity 中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class MainActivity extends AppCompatActivity {

private EditText edittext;

private String FileName = "account";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}

@Override
protected void onDestroy() {
super.onDestroy();
String s = edittext.getText().toString();
saveData(s);
}

private void initView() {
edittext = findViewById(R.id.id_edittext);
}

public void saveData(String string){
FileOutputStream out = null;
BufferedWriter writer = null;
try {
out = openFileOutput(FileName, Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(string);
}catch (Exception e){
e.printStackTrace();
}finally {
try {
if (writer != null){
writer.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
}

}

saveData()方法主要是文件流写入的基本操作,然后重写了onDestroy()方法,这样就可以保证在活动销毁之前一定会调用saveData()方法。

然后关闭程序,这时我们输入的内容就已经保存到文件中了。那么如何才能证实数据确实已经保存成功了呢?我们可以借助Device File Explorer工具来查看一下。点击Android Studio导航栏中的Tools →Tool Windows →Device File Explorer,然后找到相应目录即可。

2.3 从文件中读取数据

既然存储了数据,那肯定在某个时间段需要拿出来使用。因此我们来学习一下如何从文件中读取数据。

同样的,Context类中也提供了一个openFileInput()方法,用于从文件中读取数据。该方法只接收一个参数,即要读取的文件名,然后系统会自动到/data/data/<package name>/files/目录下去加载这个文件。

实现小案例:基于 2.2 的案例,活动创建时,从文件中读取数据到输入框。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class MainActivity extends AppCompatActivity {

private EditText edittext;

private String FileName = "account";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
String content = loadData();
if (!TextUtils.isEmpty(content)){
edittext.setText(content);
}
}

......

public String loadData(){

FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try {
in = openFileInput(FileName);
reader = new BufferedReader(new InputStreamReader(in));
String line = "";
while((line = reader.readLine()) != null){
content.append(line);
}

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

return content.toString();

}

}

loadData()方法主要是文件流读取的基本操作,然后重写了onCreate()方法,这样就可以保证在活动创建时一定会调用loadData()方法。在onCreate()方法中调用了TextUtils,isEmpty()方法来对读取到的字符串进行非空判断。TextUtils,isEmpty()方法可以一次性进行两种空值的判断。当传入的字符串等于null或者等于空字符串的时候,这个方法都会返回true,从而使得我们不需要先单独判断这两种空值再使用逻辑运算符连接起来了。

三、SharedPreferences存储

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

3.1 将数据存储到SharedPreferences中

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

  1. Context类中的getSharedPreferences()方法

    此方法接收两个参数:

    • 第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在/data/data/<package name>/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. 使用putXXX()方法向SharedPreferences.Editor对象中添加数据,比如添加字符串使用putString()方法。
  3. 调用apply()方法将添加的数据提交,从而完成数据存储操作。

了解完之后,开始实操。

和文件存储的案例一样,只不过使用的是不同的存储方法。(试了一下,并不可以!)

主活动布局改成一个按钮就行。

MainActivity 的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainActivity extends AppCompatActivity {

private String FileName = "account";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//按钮的点击事件
public void ClicktosaveData(View view) {
SharedPreferences sharedPreferences = getSharedPreferences(FileName, Context.MODE_PRIVATE);
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putString("name","Jack");
edit.putInt("age",28);
edit.putBoolean("married",true);
edit.apply();
}
}

我们同样依靠Device File Explorer进行查看,情况如下图所示:

3.2 从SharedPreferences中读取数据

在3.1中,我们通过一系列put方法将数据存储到SharedPreferences中,类似的,我们也可以通过一系列的get方法读取存储的数据。这些get方法都接收两个参数,第一个参数是键,传入存储数据时使用的键就可以得到相应的值了;第二个参数是默认值,即表示当传入的键找不到对应的值时会以什么样的默认值进行返回。

与存储数据不同,读取数据不需要获取SharedPreferences.Editor对象,只需要SharedPreferences对象即可,步骤如下:

  1. 获取SharedPreferences对象
  2. 通过get方法获取存储的数据

开始实操,将我们刚刚存入的数据读取出来。

主活动布局不变,只不过按钮功能从存储数据变成了还原数据。

主要修改的是MainActivity的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends AppCompatActivity {

private String FileName = "account";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void ClicktoRestoreData(View view) {
SharedPreferences sharedPreferences = getSharedPreferences(FileName, MODE_PRIVATE);
String name = sharedPreferences.getString("name","");
int age = sharedPreferences.getInt("age",0);
boolean married = sharedPreferences.getBoolean("married",false);
Log.d("aaa","name: " + name);
Log.d("aaa","age: " + age);
Log.d("aaa","married: " + married);
}
}

3.3 实现记住密码功能

首先我们需要实现一个登陆界面的布局(activity_login.xml)以及活动(LoginActivity.java)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">

<EditText
android:layout_width="match_parent"
android:layout_height="50dp"
android:hint="账号"
android:id="@+id/zhanghao"/>
<EditText
android:layout_width="match_parent"
android:layout_height="50dp"
android:hint="密码"
android:id="@+id/mima"/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/remeber"
android:text="记住密码"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="登入"
android:onClick="ClicktoLogin"/>

</LinearLayout>

这里需要使用到CheckBox控件,以代表用户是否需要记住密码。

然后是LoginActivity.java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class LoginActivity extends AppCompatActivity {

private EditText accountEdit;
private EditText passwordEdit;
private CheckBox remeberCheckBox;
private String FileName = "loginData";
private String Arg1 = "account";
private String Arg2 = "password";
private String Arg3 = "isRemeber";

protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
initView();

isRestoreData();

}
//获取控件对象
private void initView() {
accountEdit = findViewById(R.id.zhanghao);
passwordEdit = findViewById(R.id.mima);
remeberCheckBox = findViewById(R.id.remeber);
}
//登入按钮的点击事件
public void ClicktoLogin(View view){

isSaveData();

Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
//存储数据
public void isSaveData() {
SharedPreferences loginData = getSharedPreferences(FileName, MODE_PRIVATE);
SharedPreferences.Editor loginEdit = loginData.edit();
//如果点击了记住密码的选框
if (remeberCheckBox.isChecked()){
loginEdit.putString(Arg1, accountEdit.getText().toString());
loginEdit.putString(Arg2, passwordEdit.getText().toString());
loginEdit.putBoolean(Arg3, remeberCheckBox.isChecked());
}else{//没有记住密码,就将存储的数据擦除
loginEdit.clear();
}
loginEdit.apply();
}
//恢复数据
public void isRestoreData() {
SharedPreferences sharedPreferences = getSharedPreferences(FileName, MODE_PRIVATE);
boolean isremeber = sharedPreferences.getBoolean(Arg3, false);
if (isremeber){
String account = sharedPreferences.getString(Arg1,"");
String password = sharedPreferences.getString(Arg2, "");
accountEdit.setText(account);
passwordEdit.setText(password);
remeberCheckBox.setChecked(isremeber);
}
}
}

首先在ClicktoLogin()方法中调用isSaveData()方法,在里面判断是否要保存密码,如果CheckBox被选中,则表示用户想要记住密码,那么就把account和password以及CheckBox的选择状态的值都存入到SharedPreferences文件当中并提交。如果没有被选中,就简单地调用一下clear()方法,将SharedPreferences文件中之前存储的数据全部清除掉。

当用户选中了记住密码复选框,并成功登录一次之后,这个时候如果再重新启动登录界面,就会从SharedPreferences文件中将保存的账号和密码都读取出来,并填充到文本输入框中,然后把记住密码复选框选中,这样就完成记住密码的功能了。

当然,这功能还是有缺陷的,因为不记住密码并不意味着账号也跟着清楚掉,总之还可以在这个基础上进行扩展。

MainActivity的代码和布局这里就不放出来,就是一个退出按钮,点击回到登入界面,可以说是很简单的功能。

四、SQLite数据库存储

论谁的数据存储的功能强大,那我只能说还得是数据库更厉害,前两者与之比较简直是相形见绌!Android系统内置了SQLite数据库,它是一款轻量级的关系型数据库,运算速度非常快,占用资源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。

4.1 创建数据库

Android提供了SQLiteOpenHelper帮助类,它让我们能够更加方便地管理数据库,借助这个类就可以非常简单地对数据库进行创建和升级。

我们需要创建一个类来继承SQLiteOpenHelper帮助类,它有两个抽象方法,分别是onCreate()onUpgrade(),我们必须在自己的类里面重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。

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

SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可。这个构造方法中接收4个参数:

  • 第一个参数是Context,必须要有它才能对数据库进行操作。
  • 第二个参数是数据库名,创建数据库时使用的就是这里指定的名称。
  • 第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,一般都是传人nulL。
  • 第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。

构建出SQLiteOpenHelper的实例之后,再调用它的getReadableDatabase()getWritableDatabase()方法就能够创建数据库了,数据库文件会存放在/data/data/<package name>/databases/目录下。此时,重写的onCreate()方法也会得到执行,所以通常会在这里去处理一些创建表的逻辑。

下面我们例子来学会如何使用SQLiteOpenHelper。

假设我们要创建一个名为BookStore的数据库,并在里面创建一个名为Book的表,那么如果学过SQL的话,建表语句应该是这样的:

1
2
3
4
5
6
7
create table Book(
id integer primary key autoincrement,
author text,
price real,
pages integer,
name text
)

该表包含有关书的属性,如作者、价格、页数、书名,id作为主键存在(唯一性)。

接下来我们应该利用SQLiteOpenHelper类来创建数据库并建表,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyDatabaseHelper extends SQLiteOpenHelper {

private final String CreateBookTable = "create table Book(\n" +
" id integer primary key autoincrement,\n" +
" author text,\n" +
" price real,\n" +
" pages integer,\n" +
" name text\n" +
")";
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(CreateBookTable);
Toast.makeText(mContext, "create table succeeded", Toast.LENGTH_SHORT).show();
}

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

}
}

然后是主活动的布局和代码。布局的话,一个按钮就足够了,然后是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void ClicktoCreateDataBase(View view) {
MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(this,"BookStore.db",null,1);
myDatabaseHelper.getWritableDatabase();
}
}

很简单是不是?只需要创建一个MyDatabaseHelper对象,通过构造函数的参数将数据库名指定为BookStore.db,版本号指定为1,然后调用了getWritableDatabase()方法来创建数据库。

好!那我们怎么验证确实创建成功了呢?尝试使用Device File Explorer进行查看,发现只能看到databases目录下出现了一个
BookStore.db文件,Book表是无法通过File Explorer看到的。因此我们需要换另一种方式查看,使用adb shell来对数据库和表的创建情况进行检查。相关指令如下:

1
2
3
4
5
6
7
8
//打开数据库
sqlite3 BookStore.db
//查看所有表
.table
//查看建表语句
.schema
//退出编辑
.exit或.quit

4.2 升级数据库

如果我们之后又想创建一个Category表用于记录图书的分类,那应该怎么办呢?

我们先来试试将写入onCreate()方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class MyDatabaseHelper extends SQLiteOpenHelper {

private final String CreateBookTable = "create table Book(\n" +
" id integer primary key autoincrement,\n" +
" author text,\n" +
" price real,\n" +
" pages integer,\n" +
" name text\n" +
")";

private final String CreateCategoryTable = "create table Category(\n" +
"\tid integer primary key autoincrement,\n" +
" category_name text,\n" +
" category_code integer\n" +
")";

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(CreateBookTable);
sqLiteDatabase.execSQL(CreateCategoryTable);
Toast.makeText(mContext, "create table succeeded", Toast.LENGTH_SHORT).show();
}

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

}
}

尝试了一下,发现并没有成功创建Category表,原因是BookStore.db已经存在,之后不管我们怎么点击创建按钮(即执行getWritableDatabase()方法),MyDatabaseHelper中的onCreate()方法都不会再次执行,因此也就无法新增表。

但是我们可以重写onUpgrade()方法来实现添加新表!

1
2
3
4
5
6
7
8
9
10
11
public class MyDatabaseHelper extends SQLiteOpenHelper {

......

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);
}

}

可以看到,我们在onUpgrade()方法中执行了两条drop语句,如果发现数据库中已经存在Book表或Category表了,就将这两张表删除掉,然后再调用onCreate()方法重新创建。这里先将已经存在的表删除掉,因为如果在创建表时发现这张表已经存在了,就会直接报错。

接下来的问题就是如何让onUpgrade()方法能够执行了,还记得SQLiteOpenHelper的构造方法里接收的第四个参数吗?它表示当前数据库的版本号,之前我们传入的是1,现在只要传入一个比1大的数,就可以让onUpgrade()方法得到执行了。修改MainActivity中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void ClicktoCreateDataBase(View view) {
//版本改为2
MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
myDatabaseHelper.getWritableDatabase();
}
}

现在重新运行程序,并点击创建按钮,这时就会再次弹出创建成功的提示。当然,我们也可以通过adb shell来验证。

(但是这有个问题,这把之前的数据库给删了,那数据岂不是丢失了!!!)

4.3 添加数据

在数据库中,我们对数据进行的操作无非就是增删改查这四种。相比于在学习数据库时不断编写insert、delete、update、select语句,Android提供了一系列的辅助性方法,使得在Android中即使不去编写SQL语句,也能轻松完成所有的增删改查操作。

在前面有一点我们没有注意到,就是SQLiteOpenHelper的getReadableDatabase()getwritableDatabase()方法都会返回一个SQLiteDatabase对象,借助这个对象就可以对数据进行增删改查操作了。

那我们先来学习一下如何添加数据。SQLiteDatabase中提供了一个insert()方法,这个方法就是专门用于添加数据的。它接收3个参数:

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

那么就开始实操吧。同样还是一个按钮实现添加数据,那么它的布局就不多说了。

MainActivity中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void ClicktoInsertData(View view) {
MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
SQLiteDatabase BookDatabase = myDatabaseHelper.getWritableDatabase();
ContentValues book = new ContentValues();
//组装第一条记录
book.put("name","The Da Vinci Code");
book.put("author","Dan Brown");
book.put("pages",454);
book.put("price",16.53);
BookDatabase.insert("Book",null,book);
book.clear();
//组装第二条记录
book.put("name","The Lost Symbol");
book.put("author","Dan Brown");
book.put("pages",510);
book.put("price",17.16);
BookDatabase.insert("Book",null,book);
}
}

将上面的操作转成SQL语句就是:

1
2
insert into Book(name, author, pages, price) values("The Da Vinci Code", "Dan Brown", 454, 16.53);
......

由于id被指定为自增长,所以不需要设置id的值。然后我们可以打开BookStore数据库,执行select * from Book; 语句(不要忘记最后添加分号)来验证是否添加成功。

4.4 更新数据

更新数据使用update()方法,该方法接收4个参数:

  1. 第一个参数是表名。
  2. 第三个参数是一个ContentValues对象,把要更新的数据在这里组装进去。
  3. 第三、第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认更新所有行。

只需要稍微修改MainActivity中的代码就行了,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends AppCompatActivity {
......

public void ClicktoUpDate(View view) {
MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
SQLiteDatabase BookDatabase = myDatabaseHelper.getWritableDatabase();

ContentValues values = new ContentValues();
values.put("price",9.99);

BookDatabase.update("Book",values,"name = ?",new String[]{"The Da Vinci Code"});

}
}

从这里可以看出SQLiteDatabase的update()的第三、第四个参数的具体作用。第三个参数对应的是SQL语句的where部分,表示更新有name等于?的行,而?是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容,相当于SQL代码:

1
update Book set price = 9.99 where name = "The Da Vinci Code";

4.5 删除数据

更新数据使用delete()方法,该方法接收3个参数:

  1. 第一个参数是表名。
  2. 第二、三个参数用于约束删除某一行或某几行中的数据,不指定的话默认删除所有行。

同样的,只需要把MainActivity中的操作换成删除操作就行了。

1
2
3
4
5
6
7
8
9
public class MainActivity extends AppCompatActivity {
......

public void ClicktoCreateupDate(View view) {
MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
SQLiteDatabase BookDatabase = myDatabaseHelper.getWritableDatabase();
BookDatabase.delete("Book","pages > ?",new String[]{"500"});
}
}

对应的SQL语句为:

1
delete from Book where pages > 500;

4.6 查询语句

学过数据库的都知道,查询语句是最复杂的一种操作,因为它往往会伴随一些复杂的限制条件,以及多个表结合查询等等。

查询语句使用query()方法,因为它的复杂性,所以参数有很多。最短的一个方法也需要传入7个参数,分别如下:

拿个简单的例子试试手。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MainActivity extends AppCompatActivity {
......

public void ClicktoCreateupDate(View view) {
MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(this,"BookStore.db",null,3);
SQLiteDatabase BookDatabase = myDatabaseHelper.getWritableDatabase();
Cursor book = BookDatabase.query("Book", null, "author = ?", new String[]{"Dan Brown"}, null, null, null);
if (book.moveToFirst()){
do {
String name = book.getString(book.getColumnIndex("name"));
String author = book.getString(book.getColumnIndex("author"));
int pages = book.getInt(book.getColumnIndex("pages"));
double price = book.getDouble(book.getColumnIndex("price"));
Log.d("aaa", "name: " + name);
Log.d("aaa", "author: " + author);
Log.d("aaa", "pages: " + pages);
Log.d("aaa", "price: " + price);
}while (book.moveToNext());
}
book.close();
}
}

相当于SQL语句:

1
select * from Book where author = "Dan Brown";

moveToFirst()方法将数据的指针移动到第一行的位置,然后进入了一个循环当中,去遍历查询到的每一行数据。在这个循环中可以
通过Cursor的getColumnIndex()方法获取到某一列在表中对应的位置索引,然后将这个索引传入到相应的取值方法中,就可以得到从数据库中读取到的数据了。

4.7 使用SQL操作数据库

如果我们不习惯使用Android提供的方法来对数据进行操作时,我们也可以直接执行SQL语句来实现对数据的操作。

现在对这四种操作举一些例子来理解如何使用(假定数据库对象为db)。

  • 添加数据

    1
    db.execSQL("insert into Book (name,author,pages,price) values(?,?,?,?)", new String[]{"The Da Vinci Code","Dan Brown","454","16.96"})
  • 跟新数据

    1
    db.execSQL("update Book set price = ? where name = ?", new string[]{"10.99", "The Da Vinci Code"});
  • 删除数据

    1
    db.execSQL("delete from Book where pages > ?", new String[]{"500"});
  • 查询数据

    1
    2
    db.rawQuery("select * from Book",null);
    rawQuery("select * from Book where author = ?", new String[]{"Dan Brown"})

简而言之,第一个参数是通配SQL语句,第二个参数是填充通配符的值(按顺序填写)。

五、使用LitePal操作数据库

LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发最常用到的一些数据库功能进行了封装,使得不用编写一行SQL语句就可以完成各种建表和增删改查的操作。LitePal的项目主页上也有详细的使用文档,地址是:https://github.com/guolindev/LitePal

5.1 配置LitePal

那么怎样才能在项目中使用开源库呢?过去的方式比较复杂,通常需要下载开源库的 Jar 包或者源码,然后再集成到我们的项目当中。而现在就简单得多了,大多数的开源项目都会将版本提交到 jcenter 上,我们只需要在 app/build.gradle 文件中声明该开源库的引用就可以了。

因此,要使用LitePal的第一步,就是编辑app/build.gradle文件,在dependencies闭包中添加如下内容:

1
2
3
dependencies {
implementation 'org.litepal.guolindev:core:3.2.3'
}

添加的这一行声明中,前面部分是固定的,最后的3.2.3是版本号的意思,最新的版本号可以到LitePal的项目主页上去查看。

接下来就可以开始build了。但是会报错:

1
2
3
4
5
6
7
8
Execution failed for task ':app:mergeDebugNativeLibs'.
> Could not resolve all files for configuration ':app:debugRuntimeClasspath'.
> Could not find org.litepal.guolindev:core:3.2.3.
Searched in the following locations:
- https://dl.google.com/dl/android/maven2/org/litepal/guolindev/core/3.2.3/core-3.2.3.pom
- https://repo.maven.apache.org/maven2/org/litepal/guolindev/core/3.2.3/core-3.2.3.pom
Required by:
project :app

这个报错告诉我们在https://dl.google.com/dl/android/maven2和https://repo.maven.apache.org/maven2网站上找不到LitePal。但问题是我们明明知道LitePal在Github上,但它却在这两个网站上查找。为了修改它的下载路径,我们需要在外层目录下的`settings.gradle`文件中的 repositories 块中添加以下代码:

1
2
jcenter()
maven { url 'https://jitpack.io' }

再次build就把LitePal成功引入到当前项目中了,接下来需要配置litepal.xml文件。在app/src/main目录下创建一个assets目录,然后在assets目录下再新建一个litepal.xml文件(选择File,命名的时候加文件后缀),接着编辑litepal.xml文件中的内容,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<!--
Define the database name of your application.
By default each database name should be end with .db.
If you didn't name your database end with .db,
LitePal would plus the suffix automatically for you.
For example:
<dbname value="demo" />
-->
<dbname value="BookStore" />

<!--
Define the version of your database. Each time you want
to upgrade your database, the version tag would helps.
Modify the models you defined in the mapping tag, and just
make the version value plus one, the upgrade of database
will be processed automatically without concern.
For example:
<version value="1" />
-->
<version value="3" />

<!--
Define your models in the list with mapping tag, LitePal will
create tables for each mapping class. The supported fields
defined in models will be mapped into columns.
For example:
<list>
<mapping class="com.test.model.Reader" />
<mapping class="com.test.model.Magazine" />
</list>
-->
<list>
</list>

<!--
Define where the .db file should be. "internal" means the .db file
will be stored in the database folder of internal storage which no
one can access. "external" means the .db file will be stored in the
path to the directory on the primary external storage device where
the application can place persistent files it owns which everyone
can access. "internal" will act as default.
For example:
<storage value="external" />
-->

</litepal>

其中,<dbname>标签用于指定数据库名,<version>标签用于指定数据库版本号,<List>标签用于指定所有的映射模型,我们稍后就会用到。

最后在AndroidManifest.xml文件中添加:

1
2
3
4
5
6
7
8
<manifest>
<application
android:name="org.litepal.LitePalApplication"
...
>
...
</application>
</manifest>

至此,我们就完成了LitePal的环境搭建!

5.2 创建和升级数据库

刚才在介绍的时候已经说过,LitePal采取的是对象关系映射(ORM)的模式,那么什么是对象关系映射呢?简单点说,我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,那么将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射了。

之前为了创建一张Book表,需要先分析表中应该包含哪些列,然后再编写出一条建表语句,最后在自定义的SQLiteOpenHelper中去执行这条建表语句。但是使用LitePal就可以用面向对象的思维来实现同样的功能了,定义一个Book类,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class Book extends LitePalSupport {

private int id;
private String author;
private double price;
private int pages;
private String name;

public Book(int id, String author, double price, int pages, String name) {
this.id = id;
this.author = author;
this.price = price;
this.pages = pages;
this.name = name;
}

public Book() {
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

public int getPages() {
return pages;
}

public void setPages(int pages) {
this.pages = pages;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

这是一个典型的Java bean,在Book类中我们定义了id、author、price、pages、name这几个字段,并生成了相应的getter和setter方法。学过java web的应该知道,Book类对应的是数据库中的Book表,而类中的每一个字段分别对应了表中的每一个列,这就是对象关系映射最直观的体验。

接下来我们还需要将Book类添加到映射模型列表当中,修改litepal.xml中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<litepal>

<dbname value="BookStore" />

<version value="1" />

<list>
<mapping class="com.example.testlitepal.Book"/>
</list>

</litepal>

这里使用<mapping/>标签来声明我们要配置的映射模型类,注意一定要使用完整的类名。不管有多少模型类需要映射,都使用同样的方式配置在<list>标签下即可。
到这里我们就已经把所有工作都完成了,现在只要进行任意一次数据库的操作,BookStore.db数据库应该就会自动创建出来。那么我们修改MainActivity中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void ClicktoCreateDataBase(View view) {
LitePal.getDatabase();
}
}

Connector.getDatabase()方法是一次最简单的数据库操作,只要点击一下按钮,数据库就会自动创建完成了。运行一下程序,然后点击Create database按钮,通过adb shell查看,如下图所示:

(一直在查看BookStore文件,以为没创建出表,最后才发现应该查看BookStore.db文件!!!)

5.3 升级数据库

在 4.2 的最后,我留了个疑问,就是删除之前的数据库可能会造成数据丢失的问题。然而在LitePal中,我们并不需要担心这个问题,我们只需要添加或者修改目标内容,然后将版本号 +1 就行了。

比如我们想要向Book表中添加一个press(出版社)列,那么直接在Book类中添加一个press字段即可并设置好get和set方法即可(如果有带参的构造函数,记得在参数中添加新属性),代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Book extends LitePalSupport {

......
private String press;

......
public String getPress() {
return press;
}

public void setPress(String press) {
this.press = press;
}
}

与此同时,我们再添加一张Category表,那么只需要创建一个Category类即可,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Category extends LitePalSupport {

private int id;
private String categoryName;
private int categoryCode;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getCategoryName() {
return categoryName;
}

public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}

public int getCategoryCode() {
return categoryCode;
}

public void setCategoryCode(int categoryCode) {
this.categoryCode = categoryCode;
}
}

改完了所有我们想改的东西,只需要记得将版本号 +1 就行了。当然由于这里还添加了一个新的模型类,因此也需要将它添加到映射模型列表中。修改litepal.xml中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<litepal>

<dbname value="BookStore" />

<version value="2" />

<list>
<mapping class="com.example.testlitepal.Book"/>
<mapping class="com.example.testlitepal.Category"/>
</list>

</litepal>

重新运行程序,点击按钮,通过查看可以确认成功了。

(这里是安装了一个插件,感觉更加方便,可以查看表的结构和数据。双击文件的时候会有弹窗,然后那个插件就在里面,专门用来打开数据库的)

5.4 添加数据

对于增删改操作,模型类必须要继承LitePalSupport类才行。之后我们只需要创建出模型类的实例,再将所有要存储的数据设置好,最后调用一下save()方法就可以了。

好在我们写的代码已经继承了,所以代码直接复用就行。因此我们只需要修改MainActivity代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends AppCompatActivity {
......

public void ClicktoAddData(View view) {
//创建实体类
Book book = new Book();
//设置属性值
book.setName("The Da Vinci Code");
book.setAuthor("Dan Brown");
book.setPages(454);
book.setPrice(16.92);
book.setPress("Unknow");
//向Book表中添加数据
book.save();
}
}

当然也可以直接使用构造函数设置属性值。重新运行程序,可以发现数据添加成功。

5.5 更新数据

更新数据要比添加数据稍微复杂一点,因为它的API接口比较多,这里我们只介绍最常用的几种更新方式。

首先,最简单的一种更新方式就是对数据库中已存储的对象重新设值,然后重新调用save()方法即可。例子如下所示:

1
2
3
4
5
6
7
8
public void ClicktoUpData(View view) {
//在Book表中找到id为1的记录
Book book = LitePal.find(Book.class,1)
//设置要修改的值
book.setPrice(10.99);
//提交
book.save();
}

其中LitePal.find(model.class,id)方法的第一个参数为表对应的类,第二个为要更新的记录的id。

第二种方法是使用model.update(id)model.updateAll(conditions)方法,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
public void ClicktoUpData(View view) {

Book book = new Book();

book.setPrice(9.99);
book.setPress("Anchor");
//更新id=1的记录
book.update(1);
//更新页数为510页的记录
book.updateAll("pages = ?","510");
}

其中update()方法指定id即可,updateAll()方法可以指定一个或多个约束条件,如果不指定条件语句的话,就表示更新所有数据。

当你想把一个字段的值更新成默认值时,是不可以使用上面的方式来设置数据的。在Java中任何一种数据类型的字段都会有默认值,当我们new出一个Book对象时,其实所有字段都已经被初识化成默认值了,比如pages默认为0,因此,如果我们想把数据库表中的pages列更新成0,直接调用book.setPages(0)是不可以的,因为即使不调用这行代码,pages字段本身也是0,LitePal此时是不会对这个列进行更新的。

对于将数据更新成默认值的操作,LitePal统一提供了一个setToDefault()方法,然后传入相应的列名就可以了实现了。
比如我们可以这样写:

1
2
3
Book book = new Book();
book.setToDefault("pages");
book.updateAll();

这段代码的意思是,将所有书的页数都更新为0,因为updateAll()方法中没有指定约束条件,因此更新操作对所有数据都生效了。

5.6 删除数据

删除数据有两种方法:LitePal.delete(model.class,id)方法和LitePal.deleteAll(model.class,conditions)方法。示例如下:

1
2
3
4
5
6
public void ClicktoCreateDataBase(View view) {
//删除Book表中id=1的记录
LitePal.delete(Book.class, 1);
//删除Book表中name = "Dan Brown"的记录
LitePal.deleteAll(Book.class, "name = ?" , "Dan Brown");
}

5.7查询数据

对于查询数据,LitePal提供了find()findAll()select()where()order()limit()offset()方法进行组合查询,

  • select():用于指定查询哪几列的数据,对应了SQL当中的select关键字。比如只查name和author这两列的数据,就可以这样写:

    1
    List<Book> books = LitePal.select("name","author").find(Book.class); 
  • where()方法:用于指定查询的约束条件,对应了SQL当中的where关键字。比如只查页数大于400的数据,就可以这样写:

    1
    List<Book> books = LitePal.where("pages > ?","400").find(Book.class);
  • order()方法:用于指定结果的排序方式,对应了SQL当中的order by关键字。比如将查询结果按照书价从高到低排序,就可以这样写:

    1
    List<Book> books = LitePal.order("price desc").find(Book.class);

    其中desc表示降序排列,asc或者不写表示升序排列。

  • limit()方法:用于指定查询结果的数量,比如只查表中的前3条数据,就可以这样写:

    1
    List<Book> books = LitePal.limit(3).find(Book.class);
  • offset()方法:用于指定查询结果的偏移量,比如查询表中的第2条、第3条、第4条数据,就可以这样写:

    1
    List<Book> books = LitePal.limit(3).offset(1).find(Book.class);

    由于limit(3)查询到的是前3条数据,这里我们再加上offset(1)进行一个位置的偏移,就能实现查询第2条、第3条、第4条数据的功能了。limit()offset()方法共同对应了SQL当中的limit关键字。

当然,我们还可以对这5个方法进行任意的连缀组合,来完成一个比较复杂的查询操作,示例如下:

1
2
3
4
5
6
7
8
9
10
11
//查找单条记录
Book book = LitePal.find(Book.class, 1);
//查找所有记录
List<Book> booklist = LitePal.findAll(Book.class);
//查询Book表中第11~20条满足页数大于400这个条件的name、author和pages这3列数据,并将查询结果按照页数升序排列。
List<Book> booklist = LitePal.select("name","author","pages")
.where("pages > ?", "400")
.order("pages")
.limit(10)
.offset(10)
.find(Book.class);

除此之外,LitePal仍然支持使用原生的SQL来进行查询:

1
Cursor c = LitePal.findBySQL("select * from Book where pages > ? and price < ?","400","20");

其中LitePal.findBySQL()方法的第一个参数用于指定SQL语句,后面的参数用于指定占位符的值。注意findBySQL()方法返回的是一个Cursor对象,接下来还需要通过之前所学的老方式将数据一一取出才行。


Android开发学习笔记(七) —— 详解数据存储全方案
http://example.com/2023/08/12/Android安全/Android开发学习笔记(七) —— 详解数据存储全方案/
作者
gla2xy
发布于
2023年8月12日
许可协议