第一行代码(六)

第六章内容主讲数据持久化技术

一、数据持久化技术简介

  数据持久化技术就是将瞬时数据(存储在内存中,有可能会因为程序关闭或其他原因导致内存被回收而丢失的数据)保存到存储设备中,保证即使在手机或电脑关机的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的,持久化技术则提供了一种机制可以让数据在瞬时状态和持久状态之间进行转换。
  Android 系统中主要提供了3种方式用于实现数据持久化功能:文件存储、SharedPreference 存储以及数据库存储。当然还可以保存在 SD 卡中,不过相比之下,SD卡比较麻烦而且不安全。

二、文件存储

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

  • 将数据存储到文件中
      Context 提供了一个 openFileOutput()方法,可以用于将数据存储到指定的文件中。

    image.png

      接下来看看用流写入文件的操作吧
    image.png

       然后我们可以借助 Android Device Monitor 工具来查看一下:点击 Tools-->Android-->Android Device Monitor 打开可视化工具
    image.png

    image.png

    这里我的模拟器打不开,但是后面我通过代码证明文件存储成功
       然后找到/data/data/<packageName>/files/目录下就可以看到我们生成的 file 文件了,然后点击导出按钮导出到电脑上即可。

  • 从文件中读取数据
       Context 类还提供了 openFileInput()方法,用于从文件中读取数据。

    image.png

    image.png

    image.png

    文件存储方式并不适合用于保存一些较为复杂的文本数据

三、SharedPreferenes 存储

  不同于文件存储,SharedPreferences 是使用键值对的方式来存储数据的。

  • 将数据存储到 SharedPreferences 中
       首先要获取 SharedPreferences 对象,然后添加数据,最后提交

    image.png

    注意:SharedPreferences文件是使用 XML 格式来对数据进行管理的

  • 从 SharedPreferences 中读取数据
       SharedPreferences 中提供了一系列 get 方法,用于对存储的数据进行读取,每种 get 方法都对应了 SharedPreferences.Editor 中的一种 put 方法。


    image.png

四、SQLite 数据库存储

  SQLitte 是一款轻量级的关系型数据库,运算速度快,占用资源少,通常只需要几百 KB 的内存就足够了,因而特别别适合在移动设备上使用,SQLite 不仅支持标准的 SQL 语法,还遵循了数据库的 ACID 事物。
文件存储和 SharedPreferences 存储毕竟只使用与保存一些简单的数据和键值对,当需要存储大量复杂的关系型数据的时候,就需要用数据库了
Android 为了让我们方便的管理数据库,专门提供了 SQLiteOpenHelper 帮助类,通过这个类可以非常简单地对数据库进行创建和升级

  • 创建数据库
    image.png

      SQLiteOpenHelper 还有两个非常重要的方法,getReadableDatabase()和 getWritableDatabase()。这两个方法都可以打开或创建数据库,并返回一个可以对数据库进行读写操作的对象。不同的是,当数据库不可写入的时候(比如磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而 getWritableDatabase()方法则会出现异常。
      当构建出 SQLiteOpenHelper 实例之后,在调用 getReadableDatabase()或getWritableDatabase()方法就能创建数据库了,数据库文件保存在/data/data/<packageName>/databases/目录下,此时,重写的 onCreate()方法也会得到执行。
    image.png

      可以用 File Explorer 查看 databases 目录下多了一个 BookStore.db 文件,但是 Book 表是无法通过 File Explorer 看到的,需要使用 adb shell 来对数据库和表的创建情况进行查看,当然使用 adb 命令需要进行配置,需要把 platform-tools 目录配置进去
      接下来就可以使用 adb 命令进行查看了,这里如果用真机的话,可能需要 root 和管理员权限,请看本章最后小技巧。打开命令行界面,输入
    adb shell
    进入到设备控制台,如图:
    image.png

    然后通过 cd 命令进入项目的文件夹下,使用 ls 命令查看目录里的文件。
    image.png

    然后进入到 database 文件夹中,继续查看
    image.png

    该目录下有两个数据库文件,一个是我们创建的 BookStore.db,另一个则是为了让数据库能够支持事物而产生的临时日志文件。
      接下来要借助 sqlite 命令来打开数据库了,需要输入
    sqlite3 数据库名
    这里因为我用的是真机,真机文件中没有包含 sqlite3文件,所以无法使用 sqlite3命令(有办法解决,看另一篇文章:),如果你用的是模拟器,则可以。而我使用的办法是导出数据库 DB 文件,用Navicat Premium 来查看数据库文件,至于 sqlite 命令,我放到最后了。
  • 升级数据库
      当时重写的还有一个方法,onUpgrade()是用于对数据库进行升级的,假如说,现已有一张 Book 表,但是我们想添加一个 Category 表,如图:


    image.png

      当我们点击按钮,没有弹出成功提示,文件夹中也没有多出 Category.db 文件,表示 Category 表没有创建成功,如果你卸载掉程序,然后重新运行,再点击按钮创建数据库,则 Category 表创建成功。这是因为图中情况,Book 表已经存在,MyDatabaseHelper 中的 onCreate()方法都不会再执行,这时需要用到 onUpgrade()方法了。


    image.png

注意:这里之所以先要删除,是因为如果创建的时候表已经存在了,就会直接报错。

  要让 onUpgrade()方法执行,就是改变 SQLiteOpenHelper 构造方法的数据库版本号,改成比以前大的数即可。


image.png

image.png

  这里 Category 表创建成功了,就表明我们升级功能起了作用。

  • 添加数据
      其实对数据库的操作无非就是 CRUD,C 代表添加(Create),R 代表查询(Retrieve),U 代表更新(Update),D代表删除(Delete),每种操作都对应了一种 SQL 命令,Android 提供了一系列辅助性方法,使得 Android 中即使不去编写 SQL 语句,也能完成 CRUD的操作。
      前面我们知道,调用 SQLiteOpenHelper 的 getReadableDatabase()或getWritableDatabase()方法可用于创建或升级数据库,这两个方法还返回了一个 SQLiteDatabase 对象,借助这个对象,我们就可以进行一系列 CRUD 的操作了。
      SQLiteDatabase 中提供了一种 insert()方法,专门用于添加数据。
    image.png

    image.png
  • 更新数据库
      多的不说,update()方法,你懂得
findViewById(R.id.tv_update).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase sqLiteDatabase = dbHelper.getWritableDatabase();
                ContentValues contentValues = new ContentValues();
                contentValues.put("price",10.99);
                /**
                 * 更新数据库,update()方法
                 * 参1:表名    参2:ContentValue 存放数据的对象
                 * 参3:对应 SQL 的 where 部分,而 ? 是一个占位符
                 * 参4:字符串数组,为参3中的占位符指定相应的内容
                 * 其实这里参3和参4很好理解,如果用 sql 语句是这样写的:
                 * update Book set price = '10.99' where name = 'The Da Vinci Code'
                 */

                sqLiteDatabase.update("Book",contentValues,"name = ?",new String[]{"The Da Vinci Code"});
            }
        });
image.png
  • 删除数据
      删除数据,delete()方法
findViewById(R.id.tv_delete).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase sqliteDatabase = dbHelper.getWritableDatabase();
                /**
                 * 删除数据,delete()方法
                 * 对应的 sql 语句:
                 * delete from Book where pages > '500'
                 */
                sqliteDatabase.delete("BOOK","pages > ?",new String[]{"500"});
            }
        });
  • 查询数据
      查询数据是 CRUD中最复杂的操作,使用 query()方法
findViewById(R.id.tv_query).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase sqliteDatabase = dbHelper.getWritableDatabase();
                /**
                 * 查询,用 query()方法,用几个重载的方法,这里说的是参数最少的 query()方法
                 * 参1:表名                  参2:要查询表中的那几列
                 * 参3:where 后面的约束条件   参4:where约束条件占位符对应的具体的值
                 * 参5:groupBy              参6:having
                 * 参7:orderBy
                 *
                 * 返回一个 Cursor 对象,查询到的所有数据都存放在 Cursor 中
                 */
                Cursor cursor = sqliteDatabase.query("Book", null, null, null, null, null, null);
                //先将 Cursor 数据指针移动到第一行的位置
                if(cursor.moveToFirst()){
                     do{
                         //遍历 Cursor 对象,取出数据并打印
                         /**
                          * 解释一下如何获取 name 的,首先,int nameIndex = cursor.getColumnIndex("name");
                          * 会返回"name"该列在表中所对应的位置索引,然后将这个位置索引传入到
                          * cursor.getString(nameIndex)中,就可以获取对应的值了
                          */
                         String name = cursor.getString(cursor.getColumnIndex("name"));
                         String author = cursor.getString(cursor.getColumnIndex("author"));
                         int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                         double price = cursor.getDouble(cursor.getColumnIndex("price"));

                         Log.d(TAG,"name = " + name);
                         Log.d(TAG,"author = " + author);
                         Log.d(TAG,"pages = " + pages);
                         Log.d(TAG,"price = " + price);
                     }while(cursor.moveToNext());
                }
                /**
                 * 记住Cursor 使用完后一定要关闭
                 */
                cursor.close();
            }
        });
image.png
  • 使用SQL操作数据库
      有些人是 SQL 大牛,在 Android 可以直接通过 SQL 来操作数据库。

1、 添加数据:

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

2、更新数据:

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

3、删除数据:

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

4、查询数据:

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

  可以看到,除了查询的时候用到的是 rawQuery()方法,其他的全是 execSQL()方法。

五、使用 LitePal 操作数据库

  LitePal 是开源的 Android 数据库框架,采用了对象关系映射(ORM)的模式,并将数据库功能进行了封装。LitePal 的主页是:

https://github.com/LitePalFramework/LitePal

  • 配置 LitePal
      其实在 LitePal 的主页中,已经有非常详细的配置信息了,这里大概说一下吧。首先,大多数的开源项目都会将版本提交到 jcenter 上,我们需要在 app/build.gradle 文件中声明该开源库的引用即可。
    image.png

      然后需要配置litepal.xml文件,app/src/main目录下 New --> Directory,创建一个 assets 目录,然后在 assets 目录下创建一个 litepal.xml 文件,然后编辑该文件
<?xml version='1.0' encoding="utf-8"?>
<litepal>
    <!--指定数据库名-->
    <dbname value="BookStore"></dbname>
    <!--指定数据库版本号-->
    <version value = "1"></version>

    <!--指定所有映射模式-->
    <list>

    </list>
</litepal>

  最后配置一下 LitePalApplication,修改 AndroidMainifest.xml 中的代码:


image.png

注意:这里是不需要自定义 Application 的,我们看 application 标签中的 name 属性,指向的是 org.litepal 中的某个 Application,也就是说这个 LitePalApplication,LitePal 已经为我们创建好了。
  还有一个问题,如果说我的项目已经有自定义的 Application 了,但是我又需要使用 LitePal 怎么办?很简单,我们可以用另一种方式:可以在我们自定义的 Application 的 onCreate() 方法中调用 LitePalApplication.initialize(context)方法即可,这种写法就相当于把全局的 Context 对象通过参数传递给了 LitePal,效果和在清单文件中配置 LitePalAppliction 是一模一样的。

六、LitePal 的使用

  • 创建和升级数据库
      LitePal 采取的是对象关系映射(ORM)的模式,简单点说,我们使用的编程语言是面向对象的语言,使用的数据库是关系型数据库,将面向对象的语言和面向关系的数据库之间建立一种映射关系,就是对象关系映射了,我们可以使用面向对象的思维来操作数据库。
      首先创建一个 Book 实体类,并提供 getter 和 setter 方法。然后将 Book 类添加到映射模型列表中(修改litepal.xml文件)。
/**
 * Book 实体类
 */

public class Book {

    private int id;
    private String author;
    private double price;
    private int pages;
    private String name;
    
    //下面是 getter 和 setter 方法,代码就不贴出来了
}
image.png

  现在只要进行任意一次数据库的操作,BookStore.db 数据库就会自动创建出来。


image.png

  这里创建数据库就会成功。如果我们想升级数据库,也非常方便,比如,我们在 Book 表中添加一个 press(出版社)字段,修改 Book 类,添加press 属性和 getter/setter 方法。同时,我们想再添加一张 Category 表,进行如下操作。


image.png
/**
 * Category 实体类
 */

public class Category {

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

    //同样,下面是 getter 和 setter 方法
    
}
image.png

  点击按钮,Book 表中的 press 字段以及 Category 表也创建成功了。

LitePal会自动帮我们做了一项非常重要的工作,就是保留之前表中的所有数据,这样就不用担心数据丢失的问题了。

  • 添加数据
      观察一下现有的实体类,他们没有任何继承结构。因为 LitePal 进行表管理操作的时候不需要模型类有任何的继承结构,但是进行 CRUD 就不行了,必须要继承自 DataSupport 类才行。这里继承的代码就不贴出来了。
    image.png

    image.png
  • 更新数据
      最简单的更新方式就是对已存储的对象重新设值,然后重新调用 save()方法。对于 LitePal 来说,对象是否已存储是根据调用 model.isSave()方法的结果来判断的,true 表示已存储,false 表示未存储。如果已经调用过 model.save()方法去添加数据的话,此时 model 对象会被认为是已存储的对象;或者model 对象是通过 LitePal 提供的查询 API 查出来的,也会被认为是已存储的对象。
findViewById(R.id.tv_update).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Book book = new Book();
                book.setName("The Lost Symbol");
                book.setAuthor("Dan Brown");
                book.setPages(510);
                book.setPrice(19.95);
                book.save();//这个 save 是添加一条数据
                book.setPress("10.99");
                /*
                 * 这个 save() ,此时 LitePal 会发现当前的 Book 对象是已
                 * 存储的,因此不会再向数据库中去添加一条数据,而是会直接更新
                 * 当前的数据。
                 * 但是这种更新方式只能对已存储的对象进行操作,限制性比较大。
                 */
                book.save();
            }
        });

  下面介绍一种更加灵巧的更新方式。

findViewById(R.id.tv_update).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Book book = new Book();
                //设置需要更新的数据
                book.setPrice(14.95);
                book.setPress("Anchor");
                /*
                    执行更新操作,需要注意的是,updateAll()方法中可以指定一个条件约束,
                    即 where 后面的条件部分,如果不指定条件语句的话,就表示更新所有数据。
                 */
                book.updateAll("name = ? and author = ?","The Lost Symbol","Dan Brow");
            }
        });

这里有一点需要注意,如果想把一个字段设置成默认值,是不可以使用上面 set 方法来设置数据的。比如说,我们想把 pages 字段设置成0,不能直接调用 book.setPages(0);因为即使不调用这个 set 方法,pages 的默认值也是0,LitePal 是不会对这个列进行更新的。如果你想要设置成默认值,就调用 book.setToDefault("pages");方法。

  • 删除数据
      使用 LitePal 删除数据的方式主要有两种,第一种直接调用已存储对象的 delete()方法即可。第二种是使用 DataSupport.deleteAll()方法来删除数据。
findViewById(R.id.tv_delete).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                /*
                    参1表示删除哪张表中的数据,后面的参数分别代表约束条件和占位符的值
                    注意:如果该方法不指定约束条件,就意味着要删除表中所有的数据。
                 */
                DataSupport.deleteAll(Book.class,"price < ?","15");
            }
        });
  • 查询数据
      LitePal 的查询相比 SQLiteDataBase 中的查询要方便的多。

1.如果你要查询表中的所有数据,只需要这样写:
List<Book> books = DataSupport.findAll(Book.class);
2.查询 Book 表中的第一条数据:
Book firstBook = DataSupport.findFirst(Book.class);
3.查询 Book 表中的最后一条数据:
Book lastBook = DataSupport.findLast(Book.class);
  还可以通过连缀查询定制更多查询功能
1.指定查询哪几列数据:
List<Book> books = DataSupport.select("name","author").find(Book.class);
2.指定查询的约束条件
List<Book> books = DataSupport.where("pages > ?","400").find(Book.class);
3.指定结果的排序方式
List<Book> books = DataSupport.order("price desc").find(Book.class);这里 desc 表示降序,asc 或者不写表示升序排序。
4.指定查询结果的数量:查询前3条数据
List<Book> books = DataSupport.limit(3).find(Book.class);
5.指定查询结果的偏移量:查询第2、3、4条数据:
List<Book> books = DataSupport.limit(3).offset(1).find(Book.class);
6.进行组合连缀查询:

List<Book> books = DataSupport.select("name","author","pages").where("pages > ?","400").order("pages").limit(10).offset(10).find(Book.class);

7.LitePal 任然支持 SQL 来进行查询

/*
  参1指定 SQL 语句,参2指定占位符的值。
  注意:findBySQL()方法返回的是一个 Cursor 对象。
*/
Cursor c = DataSupport.findBySQL("select * from Book where pages > ?","400");

小结:

文件存储适用于存储一些简单的文本数据或者二进制数据,SharedPreferences 适用于存储一些键值对,数据库存储适用于存储一些复杂的关系型数据。

小技巧:

  • EditText 我们可以调用它的 setSelection(int length)方法将输入的光标移动到文本的末尾位置,以便于用户继续输入。
  • 使用 TextUtils.isEmpt(String str)方法可以一次性进行两种空值的判断,当传入的字符串等于 null 或者等于空字符串的时候,这个方法都会返回 true.
  • 调用 SharedPreferences.Editor 的 clear()方法,可以将对应的 SharedPreferences 文件中的数据全部清除掉。
  • 上面查看数据库文件,我是用真机测试的,首先手机需要 root,然后你可能会出现data 文件夹打不开的情况,这时要获得管理员权限,首先输入 adb shell命令,然后获得管理员权限,输入 su,接着输入
chmod 777 /data /data/data /data/data/<packageName> /data/data/<packageName>/*

即可

  • sqlite 命令:输入 sqlite3 数据库名,这时就已经打开数据库了,
    .table:查看当前数据库中有哪些表
    .schema:查看建表语句
    .exit.quit:退出数据库编辑

下一篇文章 https://www.jianshu.com/p/1e2cb6004c90

推荐阅读更多精彩内容