第五章 持久化
瞬时数据就是指那些存储在内存当中,有可能会因为程序关闭或其他原因导致内存被回收而丢失的数据。这对于一些关键性的数据信息来说是绝对不能容忍的,谁都不希望自己刚发出去的一条微博,刷新一下就没了吧。那么怎样才能保证一些关键性的数据不会丢失呢?这就需要用到数据持久化技术了。
1. 持久化技术简介
数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或电脑关机的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的,持久化技术则提供了一种机制可以让数据在瞬时状态和持久状态之间进行转换。
Android系统中主要提供了3种方式用于简单地实现数据持久化功能,即文件存储、SharedPreferences存储以及数据库存储。当然,除了这3种方式之外,你还可以将数据保存在手机的SD卡中,不过使用文件、SharedPreferences或数据库来保存数据会相对更简单一些,而且比起将数据保存在SD卡中会更加地安全。
2.文件存储
文件存储是Android中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有数据都是原封不动地保存到文件当中的,因而它比较适合用于存储一些简单的文本数据或二进制数据。如果你想使用文件存储的方式来保存一些较为复杂的文本数据,就需要定义一套自己的格式规范,这样可以方便之后将数据从文件中重新解析出来。
2.1 将数据存储到文件中
Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。
- 第一个参数是文件名,在文件创建的时候使用的就是这个名称,注意这里指定的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data//files/目录下的。
- 第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATE和MODE_APPEND。其中MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖原文件中的内容,而MODE_APPEND则表示如果该文件已存在,就往文件里面追加内容,不存在就创建新文件。
其实文件的操作模式本来还有另外两种:MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE,这两种模式表示允许其他的应用程序对我们程序中的文件进行读写操作,不过由于这两种模式过于危险,很容易引起应用的安全性漏洞,已在Android 4.2版本中被废弃。
openFileOutput()方法返回的是一个FileOutputStream对象,得到了这个对象之后就可以使用Java流的方式将数据写入到文件中了。
- 首先写一个界面搞一个输入框
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here"/>
</LinearLayout>
- 改写逻辑修改MainActivity中的代码
public class MainActivity extends AppCompatActivity {
private EditText edit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edit = (EditText) findViewById(R.id.edit);
}
@Override
protected void onDestroy() {
super.onDestroy();
String inputText = edit.getText().toString();
save(inputText);
}
public void save(String inputText) {
FileOutputStream out = null;
BufferedWriter writer = null;
try {
out = openFileOutput("data",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();
}
}
}
}
- 首先在onCreate()方法中获取了EditText的实例
- 然后重写了onDestroy()方法,这样就可以保证在活动销毁之前一定会调用这个方法。在onDestroy()方法中我们获取了EditText中输入的内容,并调用save()方法把输入的内容存储到文件中,文件命名为data。
- 找到这个Data
点击 Device File Explorer 进入其标签页,在这里找到/data/data/com.example.filepersistencetest/files/目录,可以看到生成了一个data文件。
然后可以选中文件,鼠标右击——save as 可将这个文件导出到电脑上
2.2 从文件中读取数据
类似于将数据存储到文件中,Context类中还提供了一个openFileInput()方法,用于从文件中读取数据。这个方法要比openFileOutput()简单一些,它只接收一个参数,即要读取的文件名,然后系统会自动到/data/data//files/目录下去加载这个文件,并返回一个FileInputStream对象,得到了这个对象之后再通过Java流的方式就可以将数据读取出来了。
- 新建一个 public String load方法
public String load () {
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try {
in = openFileInput("data");
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对象
- 接着再使用InputStreamReader构建出一个BufferedReader对象
- 这样我们就可以通过BufferedReader进行一行行地读取,把文件中所有的文本内容全部读取出来,并存放在一个StringBuilder对象中,最后将读取到的内容返回就可以了。
- 在onCreate()方法中调用load()方法
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edit = (EditText) findViewById(R.id.edit);
String inputText = load();
if (!TextUtils.isEmpty(inputText)) {
edit.setText(inputText);
//将输入光标移动到文本的末尾位置以便于继续输入
edit.setSelection(inputText.length());
Toast.makeText(this,"Restoring succeeded", Toast.LENGTH_SHORT).show();
}
}
- 在onCreate()方法中调用load()方法来读取文件中存储的文本内容,如果读到的内容不为null,就调用EditText的setText()方法将内容填充到EditText里,并调用setSelection()方法将输入光标移动到文本的末尾位置以便于继续输入,然后弹出一句还原成功的提示。
- 注意,上述代码在对字符串进行非空判断的时候使用了TextUtils.isEmpty()方法,这是一个非常好用的方法,它可以一次性进行两种空值的判断。当传入的字符串等于null或者等于空字符串的时候,这个方法都会返回true,从而使得我们不需要先单独判断这两种空值再使用逻辑运算符连接起来了。
现在重新运行一下程序,刚才保存的Content字符串肯定会被填充到EditText中
文件存储方面的知识,其实所用到的核心技术就是Context类中提供的openFileInput()和openFileOutput()方法,之后就是利用Java的各种流来进行读写操作。
文件存储的方式并不适合用于保存一些较为复杂的文本数据,下面学习一下Android中另一种数据持久化的方式,它比文件存储更加简单易用,而且可以很方便地对某一指定的数据进行读写操作。
3. SharedPreferences存储
不同于文件的存储方式,SharedPreferences是使用键值对的方式来存储数据的。
也就是说,当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。而且SharedPreferences还支持多种不同的数据类型存储,如果存储的数据类型是整型,那么读取出来的数据也是整型的;如果存储的数据是一个字符串,那么读取出来的数据仍然是字符串。
这样你应该就能明显地感觉到,使用SharedPreferences来进行数据持久化要比使用文件方便很多。
3.1 将数据存储到SharedPreferences中
3.1.1 获取到SharedPreferences对象
要想使用SharedPreferences来存储数据,首先需要获取到SharedPreferences对象。Android中主要提供了3种方法用于得到SharedPreferences对象。
- Context类中的getSharedPreferences()方法
- 此方法接收两个参数,第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在/data/data//shared_prefs/目录下的。
- 第二个参数用于指定操作模式,目前只有MODE_PRIVATE这一种模式可选,它是默认的操作模式,和直接传入0效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。
- 其他几种操作模式均已被废弃,MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE这两种模式是在Android 4.2版本中被废弃的,MODE_MULTI_PROCESS模式是在Android6.0版本中被废弃的。
- Activity类中的getPreferences()方法
这个方法和Context中的getSharedPreferences()方法很相似,不过它只接收一个操作模式参数,因为使用这个方法时会自动将当前活动的类名作为SharedPreferences的文件名。 - PreferenceManager类中的getDefaultSharedPreferences()方法
这是一个静态方法,它接收一个Context参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences文件。
范例:getSharedPreferences()方法实现
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button saveData = (Button) findViewById(R.id.save_data);
saveData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
editor.putString("name","SuZhe");
editor.putInt("age",22);
editor.putBoolean("married",false);
editor.apply();
}
});
}
- 这里首先给按钮注册了一个点击事件,然后在点击事件中通过getSharedPreferences()方法指定SharedPreferences的文件名为data,并得到了SharedPreferences.Editor对象。
- 接着向这个对象中添加了3条不同类型的数据,最后调用apply()方法进行提交,从而完成了数据存储的操作。
- 进入程序的主界面后,点击一下Save data按钮。这时的数据应该已经保存成功了,证实一下,进入/data/data/com.example.sharedpreferencestest/shared_prefs/目录下,可以看到生成了一个data.xml文件。
3.1.2 向SharedPreferences文件中存储数据
得到了SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了,主要可以分为3步实现。
(1) 调用SharedPreferences对象的edit()方法来获取一个SharedPreferences.Editor对象。
(2) 向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用putBoolean()方法,添加一个字符串则使用putString()方法,以此类推。
(3) 调用apply()方法将添加的数据提交,从而完成数据存储操作。
范例:
SharedPreferences对象中提供了一系列的get方法,用于对存储的数据进行读取,每种get方法都对应了SharedPreferences.Editor中的一种put方法,比如读取一个布尔型数据就使用getBoolean()方法,读取一个字符串就使用getString()方法。
这些get方法都接收两个参数,第一个参数是键,传入存储数据时使用的键就可以得到相应的值了;第二个参数是默认值,即表示当传入的键找不到对应的值时会以什么样的默认值进行返回。
- 在SharedPreferencesTest项目的基础上继续开发,修改activity_main.xml中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/save_data"
android:text="Save data"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/restore_data"
android:text="Restore data"/>
</LinearLayout>
增加了一个还原数据的按钮,通过点击这个按钮来从SharedPreferences文件中读取数据。
2. 修改MainActivity中的代码,如下所示增加以下代码:
Button restoreData = (Button) findViewById(R.id.restore_data);
restoreData.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);
Log.d("MainActivity","name is " + name);
Log.d("MainActivity","age is " + age);
Log.d("MainActivity","married is " + married);
}
});
在还原数据按钮的点击事件中首先通过getSharedPreferences()方法得到了SharedPreferences对象,然后分别调用它的getString()、getInt()和getBoolean()方法,去获取前面所存储的姓名、年龄和是否已婚,如果没有找到相应的值,就会使用方法中传入的默认值来代替,最后通过Log将这些值打印出来。
4. 使用SQLite数据库
SQLite是一款Android Studio内置的轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务。而SQLite又比一般的数据库要简单得多,它甚至不用设置用户名和密码就可以使用。Android正是把这个功能极为强大的数据库嵌入到了系统当中,使得本地持久化的功能有了一次质的飞跃。
前面所学的文件存储和SharedPreferences存储毕竟只适用于保存一些简单的数据和键值对,当需要存储大量复杂的关系型数据的时候,你就会发现以上两种存储方式很难应付得了。比如我们手机的短信程序中可能会有很多个会话,每个会话中又包含了很多条信息内容,并且大部分会话还可能各自对应了电话簿中的某个联系人。很难想象如何用文件或者SharedPreferences来存储这些数据量大、结构性复杂的数据吧?但是使用数据库就可以做得到。
4.1 创建数据库
Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper帮助类,借助这个类就可以非常简单地对数据库进行创建和升级。
- SQLiteOpenHelper是一个抽象类,这意味着如果想要使用它的话,就需要创建一个自己的帮助类去继承它。
- SQLiteOpenHelper中有两个抽象方法,分别是onCreate()和onUpgrade(),必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。
- SQLiteOpenHelper中还有两个非常重要的实例方法:getReadableDatabase()和get-WritableDatabase()。
- 这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。
- 不同的是,当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而getWritableDatabase()方法则将出现异常。
- SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可。
- 这个构造方法中接收4个参数,第一个参数是Context,必须要有它才能对数据库进行操作。
- 第二个参数是数据库名,创建数据库时使用的就是这里指定的名称。
- 第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,一般都是传入null。
- 第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。
- 构建出SQLiteOpenHelper的实例之后,再调用它的getReadableDatabase()或getWritableDatabase()方法就能够创建数据库了,数据库文件会存放在/data/data/databases/目录下。此时,重写的onCreate()方法也会得到执行,所以通常会在这里去处理一些创建表的逻辑。
示例: - 首先新建一个DatabaseTest项目。这里我们希望创建一个名为BookStore.db的数据库,然后在这个数据库中新建一张Book表,表中有id(主键)、作者、价格、页数和书名等列,Book表的建表语句如下所示:
create table Book (
id integer primary key autoincrement,
author text,
price real,
pages integer,
name text)
SQLite不像其他的数据库拥有众多繁杂的数据类型,它的数据类型很简单,integer表示整型,real表示浮点型,text表示文本类型,blob表示二进制类型。另外,上述建表语句中我们还使用了primary key将id列设为主键,并用autoincrement关键字表示id列是自增长的。
2. 然后需要在代码中去执行这条SQL语句,才能完成创建表的操作。新建MyDatabaseHelper类继承自SQLiteOpenHelper,代码如下所示:
public class MyDatabaseHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table Book ("
+ "id integer primary key autoincrement, "
+ "author text, "
+ "price real, "
+ "pages integer, "
+ "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,"Create succeeded",Toast.LENGTH_SHORT).show();
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
}
}
可以看到,我们把建表语句定义成了一个字符串常量,然后在onCreate()方法中又调用了SQLiteDatabase的execSQL()方法去执行这条建表语句,并弹出一个Toast提示创建成功,这样就可以保证在数据库创建完成的同时还能成功创建Book表。
3. 在布局文件中创建一个按钮用于创建数据库,修改activity_main.xml中的代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent">
<Buttonandroid:layout_width="match_parent"android:layout_height="wrap_content"android:id="@+id/create_database"android:text="Create database"/>
</LinearLayout>
- 在主代码中实现按钮的逻辑,修改MainActivity中的代码
public class MainActivity extends AppCompatActivity {
private MyDatabaseHelper dbHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 1);
Button createDatabase = (Button) findViewById(R.id.create_database);
createDatabase.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dbHelper.getWritableDatabase();
}
});
}
}
这里我们在onCreate()方法中构建了一个MyDatabaseHelper对象,并且通过构造函数的参数将数据库名指定为BookStore.db,版本号指定为1,然后在Create database按钮的点击事件里调用了getWritableDatabase()方法.
这样当第一次点击Create database按钮时,就会检测到当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用MyDatabaseHelper中的onCreate()方法,这样Book表也就得到了创建,然后会弹出一个Toast提示创建成功。再次点击Create database按钮时,会发现此时已经存在BookStore.db数据库了,因此不会再创建一次。
4.2 查看数据库
如果还是使用FileExplorer,那么最多你只能看到databases目录下出现了一个BookStore.db文件,Book表是无法通过FileExplorer看到的。
因此这次我们准备换一种查看方式,使用adb shell来对数据库和表的创建情况进行检查。
adb是Android SDK中自带的一个调试工具,使用这个工具可以直接对连接在电脑上的手机或模拟器进行调试操作。它存放在sdk的platform-tools目录下,如果想要在命令行中使用这个工具,就需要先把它的路径配置到环境变量里。如果你使用的是Windows系统,可以右击计算机→属性→高级系统设置→环境变量,然后在系统变量里找到Path并点击编辑,将platform-tools目录配置进去。
配置好了环境变量之后,你可能需要重启才能生效。然后就可以使用adb工具了。
- 打开命令行界面,输入adb shell,就会进入到设备的控制台
其中,#符号是超级管理员的意思,也就是说现在你可以访问模拟器中的一切数据。如果你的命令行上显示的是$符号,那么就表示你现在是普遍管理员,需输入su命令切换成超级管理员,才能执行下面的操作。
这里出现不能切换为超级管理员的问题了:
-
配置好环境变量后,打开cmd输入adb shell 进入控制界面
-
发现是普通管理员,想利用su命令切换为超级管理员,不行
问题原因:这个问题是因为我们用的模拟器,带有了Googel play 是不允许获得管理员权限。
解决办法:要去下载一个Target是Google APIS的模拟器,打开AVD Manager,选择创建一个新的设备并给它更换镜像。
需要如下图所示的镜像:
-
尝试su命令,切换成超级管理员
-
接下来使用cd命令进入到/data/data/com.example.databasetest/databases/目录下,并使用ls命令查看到该目录里的文件,如图:
-
这个目录下出现了两个数据库文件,一个正是我们创建的BookStore.db,而另一个BookStore. db-journal则是为了让数据库能够支持事务而产生的临时日志文件,通常情况下这个文件的大小都是0字节。接下来我们就要借助sqlite命令来打开数据库了,只需要键入sqlite3,后面加上数据库名即可,如图所示
键入.table命令,可以看到,此时数据库中有两张表,android_metadata表是每个数据库中都会自动生成的,不用管它,而另外一张Book表就是我们在MyDatabaseHelper中创建的了。这里还可以通过.schema命令来查看它们的建表语句
5. 键入.exit或.quit命令可以退出数据库的编辑,再键入exit命令就可以退出设备控制台了。
4.2 升级数据库
MyDatabaseHelper中还有一个onUpgrade()方法是用于对数据库进行升级的,它在整个数据库的管理工作当中起着非常重要的作用。
这个功能可以用在你想增加一个表的时候,因为按照我们之前的逻辑结构当BookStore.db数据库已经存在时,之后不管我们怎样点击Create database按钮,MyDatabaseHelper中的onCreate()方法都不会再次执行,因此新添加的表也就无法得到创建了。
- 在onUpgrade()方法中删除旧表
public class MyDatabaseHelper extends SQLiteOpenHelper {
...
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
sqLiteDatabase.execSQL("drop table if exists Book");
sqLiteDatabase.execSQL("drop table if exists Category");
onCreate(sqLiteDatabase);
}
}
- 在MyDatabaseHelper中插入新的表
public class MyDatabaseHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table Book ("
+ "id integer primary key autoincrement, "
+ "author text, "
+ "price real, "
+ "pages integer, "
+ "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,"Create succeeded",Toast.LENGTH_SHORT).show();
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
}
}
我们在onUpgrade()方法中执行了两条DROP语句,如果发现数据库中已经存在Book表或Category表了,就将这两张表删除掉,然后再调用onCreate()方法重新创建。
这里先将已经存在的表删除掉,因为如果在创建表时发现这张表已经存在了,就会直接报错。
3. 在MainActivity中让数据库升级
public class MainActivity extends AppCompatActivity {
private MyDatabaseHelper dbHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
Button createDatabase = (Button) findViewById(R.id.create_database);
createDatabase.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dbHelper.getWritableDatabase();
}
});
}
}
还记得SQLiteOpenHelper的构造方法里接收的第四个参数吗?它表示当前数据库的版本号,之前我们传入的是1,现在只要传入一个比1大的数,就可以让onUpgrade()方法得到执行了。
可以看到我们已经执行成功
4.3 添加数据
我们可以对数据进行的操作无非有4种,即CRUD。其中C代表添加(Create), R代表查询(Retrieve), U代表更新(Update),D代表删除(Delete)。
调用SQLiteOpenHelper的getReadableDatabase()或getWritableDatabase()方法是可以用于创建和升级数据库的,不仅如此,这两个方法还都会返回一个SQLiteDatabase对象,借助这个对象就可以对数据进行CRUD操作了。
SQLiteDatabase中提供了一个insert()方法,这个方法就是专门用于添加数据的。它接收3个参数:
- 第一个参数是表名,我们希望向哪张表里添加数据。
- 第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般我们用不到这个功能,直接传入null即可。
- 第三个参数是一个ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。
- 在布局文件中又新增了一个按钮,稍后就会在这个按钮的点击事件里编写添加数据的逻辑。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/create_database"
android:text="Create database"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/add_data"
android:text="Add data"/>
</LinearLayout>
- 在按钮的点击事件中添加增加数据的逻辑,修改MainActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity {
private MyDatabaseHelper dbHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
Button createDatabase = (Button) findViewById(R.id.create_database);
createDatabase.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dbHelper.getWritableDatabase();
}
});
Button addButton = (Button) findViewById(R.id.add_data);
addButton.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("pages",454);
values.put("price",16.96);
db.insert("Book",null,values);//插入第一条数据
values.clear();;
//开始第二条数据
values.put("name","The Lost Symbol");
values.put("author","Dan Brown");
values.put("pages",510);
values.put("price",19.95);
db.insert("Book",null,values);//插入第二条数据
}
});
}
}
- 在添加数据按钮的点击事件里面,我们先获取到了SQLiteDatabase对象,然后使用ContentValues来对要添加的数据进行组装。
- 这里只对Book表里其中四列的数据进行了组装,id那一列并没给它赋值。因为在前面创建表的时候,我们就将id列设置为自增长了,它的值会在入库的时候自动生成,所以不需要手动给它赋值了。
- 接下来调用了insert()方法将数据添加到表当中,注意这里我们实际上添加了两条数据,上述代码中使用ContentValues分别组装了两次不同的内容,并调用了两次insert()方法。
接下来重新运行一下程序再点击一下Add data按钮就可以查到我们的表了,注意:这里你得先把版本号再加一,去重新删表建表更新一下,不然会崩溃。
4.4 更新数据
SQLiteDatabase中也提供了一个非常好用的update()方法,用于对数据进行更新,这个方法接收4个参数:
- 第一个参数和insert()方法一样,也是表名,在这里指定去更新哪张表里的数据。
- 第二个参数是ContentValues对象,要把更新数据在这里组装进去。
- 第三、第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认就是更新所有行。
- 首先在布局中新建一个更新数据的按钮
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/update_data"
android:text="Update data"/>
2. 修改MainActivity中的代码为按钮增加功能
Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("price",99.9);
db.update("Book",values,"name = ?",new String[]{
"The Da Vinci Code"
});
}
});
- 这里在更新数据按钮的点击事件里面构建了一个ContentValues对象,并且只给它指定了一组数据,说明我们只是想把价格这一列的数据更新成99.9。
- 然后调用了SQLiteDatabase的update()方法去执行具体的更新操作,可以看到,这里使用了第三、第四个参数来指定具体更新哪几行。第三个参数对应的是SQL语句的where部分,表示更新所有name等于?的行,而?是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容。
现在重新运行一下程序,点击一下Update data按钮后,再次输入查询语句查看表中的数据情况,可以看到,The Da Vinci Code这本书的价格已经被成功改为99.9了。
4.5 删除数据
SQLiteDatabase中提供了一个delete()方法,专门用于删除数据,这个方法接收3个参数,第一个参数仍然是表名,这个已经没什么好说的了,第二、第三个参数又是用于约束删除某一行或某几行的数据,不指定的话默认就是删除所有行。
- 在布局中新增删除按钮
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/delete_data"
android:text="Delete data"/>
2. 在MainActivity中添加删除逻辑
Button deleteButton = (Button) findViewById(R.id.delete_data);
deleteButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.delete("Book", "pages > ?", new String[]{"500"});
}
});
我们在删除按钮的点击事件里指明去删除Book表中的数据,并且通过第二、第三个参数来指定仅删除那些页数超过500页的书。
4.6 查询数据
SQL的全称是Structured Query Language,翻译成中文就是结构化查询语言。它的大部功能都体现在“查”这个字上的,而“增删改”只是其中的一小部分功能。由于SQL查询涉及的内容实在是太多了,因此在这里我不准备对它展开来讲解,而是只会介绍Android上的查询功能。
SQLiteDatabase中提供了一个query()方法用于对数据进行查询。这个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。
- 第一个参数不用说,当然还是表名,表示我们希望从哪张表中查询数据。
- 第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。
- 第三、第四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据。
- 第五个参数用于指定需要去group by的列,不指定则表示不对查询结果进行group by操作。
- 第六个参数用于对group by之后的数据进行进一步的过滤,不指定则表示不进行过滤。
- 第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。更多详细的内容可以参考下表。其他几个query()方法的重载其实也大同小异,可以自己去研究一下。
- 还是先增加按钮
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
sqLiteDatabase.execSQL("drop table if exists Book");
sqLiteDatabase.execSQL("drop table if exists Category");
onCreate(sqLiteDatabase);
}
2. 增加功能
Button queryButton = (Button) findViewById(R.id.query_data);
queryButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
//查询Book表中的所有数据
Cursor cursor = db.query("Book",null,null,null,null,null,null);
if (cursor.moveToFirst()) {
do {
//遍历Cursor对象,取出数据并打印
@SuppressLint("Range") String name = cursor.getString(cursor.getColumnIndex("name"));
@SuppressLint("Range") String author = cursor.getString(cursor.getColumnIndex("author"));
@SuppressLint("Range") int pages = cursor.getInt(cursor.getColumnIndex("pages"));
@SuppressLint("Range") 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 pages is " + pages);
Log.d("MainActivity","book price is " + price);
} while (cursor.moveToNext());
}
cursor.close();
}
});
- 首先在查询按钮的点击事件里面调用了SQLiteDatabase的query()方法去查询数据。这里的query()方法非常简单,只是使用了第一个参数指明去查询Book表,后面的参数全部为null。这就表示希望查询这张表中的所有数据,虽然这张表中目前只剩下一条数据了。
- 查询完之后就得到了一个Cursor对象,接着我们调用它的moveToFirst()方法将数据的指针移动到第一行的位置,然后进入了一个循环当中,去遍历查询到的每一行数据。在这个循环中可以通过Cursor的getColumnIndex()方法获取到某一列在表中对应的位置索引,然后将这个索引传入到相应的取值方法中,就可以得到从数据库中读取到的数据了。
- 接着我们使用Log的方式将取出的数据打印出来,借此来检查一下读取工作有没有成功完成。
- 最后别忘了调用close()方法来关闭Cursor。
现在再次重新运行程序,点击一下Query data按钮后,查看logcat的打印内容,结果如图:
[图片]
这个例子只是对查询数据的用法进行了最简单的示范,在真正的项目中你可能会遇到比这要复杂得多的查询功能,更多高级的用法还需要你自己去慢慢摸索,毕竟query()方法中还有那么多的参数我们都还没用到呢。
- 使用litePal操作数据库
现在开源的热潮让所有Android开发者都大大受益,GitHub上面有成百上千的优秀Android开源项目,很多之前我们要写很久才能实现的功能,使用开源库可能短短几分钟就能实现了。
除此之外,公司里的代码非常强调稳定性,而我们自己写出的代码往往越复杂就越容易出问题。相反,开源项目的代码都是经过时间验证的,通常比我们自己的代码要稳定得多。因此,现在有很多公司为了追求开发效率以及项目稳定性,都会选择使用开源库。本书中我们将会学习多个开源库的使用方法,而现在你将正式开始接触第一个开源库——LitePal。
LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发最常用到的一些数据库功能进行了封装,使得不用编写一行SQL语句就可以完成各种建表和增删改查的操作。LitePal的项目主页上也有详细的使用文档,地址是:https://github.com/LitePalFramework/LitePal。
5.1 配置LitePal-这里我没成功 - LitePal的第一步,就是编辑app/build.gradle文件,在dependencies闭包中添加如下内容:
dependencies {
implementation 'org.litepal.guolindev:core:3.2.3'
}
- 接下来需要配置litepal.xml文件。右击app/src/main目录→New→Directory,创建一个assets目录,然后在assets目录右击→New→File 再新建一个litepal.xml文件(.xml后缀要保留),接着编辑litepal.xml文件中的内容,如下所示:
<!--
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="1" />
<!--
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" />
-->
其中,标签用于指定数据库名,标签用于指定数据库版本号,标签用于指定所有的映射模型,稍后就会用到。 3. 最后,还需要再配置一下LitePalApplication