安卓学习日志 Day17 — Content Providers 简介

概述

实际上,在上一篇 安卓学习日志 Day16 — 在应用中使用SQLite 当中有点问题,我们的初衷并不是在 Activity 的代码中直接调用 SQLite 数据库,因为这样很容易引入错误。比如,Activity 中有错别字,就会将无效输入插入到数据库中。

而这些问题都可以 通过 ContentProvider 来避免,下面就介绍 如何 Pets 应用中使用 ContentProvider 来管理数据。

ContentProvider

我们可以引入一个名为 ContentProvider 的概念作为数据库 和 Activity 之间的一个层。

使用 ContenProvider 有多方面的好处,比如可以使用它来确保输入的数据是有效的,不过要使用 ContentProvider 就不得不介绍 URI、UriMatcher 和 ContentResolver 这些不同的东西。

ContentProvider 会在当我们想要利用其它框架类从数据库加载数据到 UI 时,提供很多方面的帮助,它会使一切更顺利 与 其他框架类完美配合。

为何使用它?

ContentProvider 提供了三大好处:

  • 提供了很好的抽象层

    现在来看看 Pets 应用的情况,目前都是直接 在 Activity 中 实例化 PetDbHelper 对象,并通过该对象打开并 执行插入和读取操作。比如,可以直接访问 PetDbHelper,以将一个体重为 7 kg的宠物插入到数据库中,PetDbHelper 会帮我们直接将该宠物插入到数据库中,所以只要知道插入的信息是正确的,这个过程就能良好运行。

    但是,万一我们打错了字(假如不小心在 Actvity 中 体重的值加了负号),这时就会将一个为 负 的体重值插入到数据库中,这显然是错误的。像这种 Activity 直接与 PetDbHelper 交互的方法,其缺陷就在于,它会将无效的数据直接插入到数据库当中。

    而这就是 ContentProvider 发挥作用之处,我们可以通过 ContentProvider 集中化数据的访问和编辑。在这个模式中,我们的 UI 代码会直接 ContentProvider 交互,而不是直接与 PetDbHelper(数据库)交互。ContentProvider 作为一个数据验证层(可以看出我们确实需要它)会在我们错误输入无效的数据值时进行验证,所以,如果数据库存在任何错误,就会在这一步被捕捉到。

    ContentProvider 作为数据源和 UI 代码之间的附加层(通常称为抽象层),这是因为 ContentProvider 会抽象化数据存储的方式 或隐藏数据存储的详情,所以 UI 代码在进入任何数据访问时,只需和 ContentProvider 进行通信,它无需关心 Provider 完成此任务的时间间隔。

    ContentProvider 会以 UI 看不到的方式对底层数据进行暗箱处理,因为 UI 不关心数据是存储在数据库中还是存储在单个文件中,甚至可以存储照片文件,而 ContentProvider 可以完美处理与 UI 代码的交互以显示这些图片。所以,如果在应用的更新版本中想将数据库换做不同的存储类型 UI 代码将保持不变,并继续与现有的 ContenProvider 交互,即 除了数据库之外如果想要对每只宠物添加图片文件也是没有任何问题的,或者即使数据存储为文本文件,而非数据库及照片文件,ContentProvider 依旧能很好地加以处理。

    UI 代码中能使用各种方法与 Provider 进行交互,在所以 CRUD 操作中 UI 代码会向 ContentProvider 调用方法,而 ContentProvider 也会向数据源调用其自身形式的代码。

    总结一下就是,ContentProvider 可帮助我们管理对有结构的数据集的访问,它可以作为 UI 代码和 数据源直接很好的抽象层,在这个抽象层中可以添加数据验证,帮助我们修改数据存储的方式,而 UI 代码始终保持不变。

  • 与其他框架类完美地配合工作

    更大的好处是,它能与其他框架类完美结合,比如每当 添加或删除一个宠物时,我们都希望在主界面显示最新的信息,这就必须每次都调用 query() 方法以获取数据库最新的内容。那么取代这种繁琐工作的方法就是可以利用一个名为 CursorLoader 的框架类,每当有宠物添加或删除时,宠物列表就会借助 CursorLoader 始终处于最新的状态,因为 CursorLoader 会在数据发生更改时自动进行检查,并在确定发生了数据变更后自动更新列表,CursorLoader 可以与 ListView 和 CursorAdapter 协作。而实现 CursorLoader 需要用到 ContentProvider ,所以 ContentProvider 和 CursorLoader 一起为我们省了很多工作,使我们不需要在发生数据更改时一次次手动执行查询

    并更新 UI。它还能与主屏幕小部件搭配使用,这个部件叫做 SyncAdapter 将数据同步到云并为应用提供搜索建议。假如团队想让 ContentProvider 以一致的方式管理对有结果数据集的访问,如果没有这个部件 就要自己执行大量的管理工作。

  • 可以对其他应用分享数据

    ContentProvider 还可以用于分享数据,当应用中存在文本数据或文件时,其他应用是无法访问的。不过可以使用 ContentProvider 将数据暴露给其他应用,这样其他应用也可以使用 ContentProvider 提供的接口 从而访问数据。并且 ContentProvider 会以安全的形式管理数据,使用获得 特定访问权限的其他应用才能访问数据

ContentProvider 原理

其中在 UI 代码和 ContentProvider 直接还要一层 ,它是 ContentResolver,下面以一个例子来解释,下图展示一个应用内部 使用 ContentProvider 管理数据的流程:

在这张流程图中,ContactEditorActivity 将使用 ContentProvider 以配合 Loader 来将数据库的数据加载到 UI 中以对我们的联系人进行编辑,那么 ContactEditorActivity 可以使用联系人的 ContentURI (这个 URI 为被访问数据的唯一标识,与 Web URI 的作用类似) 对 Resolver 调用方法,而 Resolver 会将该消息发送到对应的 Provider。这个 Provider 将向数据库发送请求,最终 Provider 会得到一些结果 ,这些 结果会被发送会 Resolver,Resolver 又将这些结果返回到 Activity 并最终显示到 UI 中。

新建 PetProvier

通过以上对 ContentProvider 的描述我们可以画出 Pets 应用中使用 ContentProvider 的基本流程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UEZQFupm-1613113441629)(.\Day17~2021-02-03.assets\ContentProvider.png)]

而实际实现时,则需要使用自定义的 PetProvider(继承自 ContentProvider),因为 ContentProvider 是一个抽象类。PetProvider 应作为 com.example.pets.data java包中一个新的 Java 文件,继承自 ContentProvider,因此它需要实现五个方法 insertqueryupdatedeletegetTypeonCreate,前四个方法分别对应 数据库操作中的 CRUD,并且需要一个全局的 PetDbHelper 对应用于访问 数据库(在 onCreate 时初始化),PetProvider 的定义如下:

package com.example.pets.data;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class PetProvider extends ContentProvider {
    
    

    public static final String LOG_TAG = PetContract.class.getSimpleName();

    /**
     * Database helper object
     */
    private PetDbHelper mDbHelper;

    @Override
    public boolean onCreate() {
    
    
        mDbHelper = new PetDbHelper(getContext());

        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
    
    
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
    
    
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
    
    
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
    
    
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
    
    
        return 0;
    }
}

最后还需要在应用清单文件 AndroidManifest.xml 中声明这个 Provider:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.pets">
	
    ………………

        <provider
            android:name=".data.PetProvider"
            android:authorities="com.example.pets"
            android:exported="false" />

    </application>

</manifest>

这里的 authorities 属性表示 内容主机名(数据库在哪),name 属性表示定义 Provider 的 Java 类,<provider> 标签会将 主机名 和 Provider 关联起来。

更改完成后确保应用依然能够正常运行(但不会有任何改变),代码更改前后差异对比

PetProvider 操作

现在回过头来让我们看看 PetProvider 中除 onCreate 外的五个方法,它有一个共同点:至少且必须要 接受一个 URI 对象,下面以 query() 方法为例进行解释。

这是因为 Activity 要使用 query() 方法调用 ContentResolver ,为了帮助 Resolver 确定最终使用哪个 Provider,则需要为 query() 方法传递一个 URI 对象,这个 URI 对象指定了要访问的资源,也就是被操作数据的所在位置。

接下来 Resolver 从这个 query() 方法获得信息后,它会使用相同的 query() 方法调用合适的 Provider(在 Pets 应用中为 PetProvider),这时候 PetProvider 的 query() 方法会将传入的 参数(projection 等)转换为 SQL 语句从数据库中执行操作 并获得一个包含查询结果的 Cursor 对象,这个 Cursor 对象最终会返回至调用了 query() 方法的 Activity。

这里可能或感觉有点混乱,因为 Activity、Resolver 和 Provider 都有各自的 insertqueryupdatedelete 方法,只是各自接受的参数不同而已,类似下表:

Activity Resolver Provider
query query(Uri) query(Uri) query(……),返回含查询结果 Cursor
insert insert(Uri, ContentValues) insert(Uri, ContentValues) insert(……),返回指向插入数据的 URI
update update(Uri, ContentValues) update(Uri, ContentValues) update(……),返回被更新行的编号 int
delete delete(Uri) delete(Uri) delete(……),返回被删除行的编号 int

所以 数据操作请求 是从 Activity 发起的,最终由 Provider 执行过后会将 执行结果 返回给 发起请求的 Activity。

Content URI

设计宠物 Content URI

在与 Provider 交流时需要将正确的 Uri 作为方法的输入参数,这是因为我们需要让 Provider 知道被访问或修改的数据是什么。

在与 Provider 交流时基本需要告诉它两件事:

  • 执行什么操作( insertqueryupdatedelete
  • 被操作的数据(整个数据表 或者 表中的某行)

其中被操作的数据就要使用 Uri 来定义。

URI 全称 Uniform Resource Identifier(代表统一资源标识符),正如名称所指 它可以标识出我们 要感兴趣资源的名称、位置 (或有时同时标识名称和位置),也就是标识被操作的数据在哪里,一个 Uri 大概像这样 content://com.android.contacts/contacts

这里可能会让人想到 URL(统一资源定位符),URL 是 URI 的子集,它用于定位 某个文件或数据在网站上的具体位置,如 https://github.com/HEY-BLOOD

而现在 Pets 应用中,将使用 URI 来标识一些数据的位置,这个位置为手机上的一个类似 SQL 的数据库文件。

而我们要使用的是 ContentUri,ContentUri 主要用与标识 Provider 中的数据,它可以指向数据库的某个部分(单个行、单个表或一组表)。它也可以指向文件,,比如文本文件、照片或其他媒体文件,下面是三个应用中针对不同 Provider 的 ContentUri 示例:

Contacts Provider Calendar Provider User Dictionary Provider
content://com.android.contacts/contacts content://com.androidcalendar/events content://user_dictionary/words

可以看出 所有 的 ContentUri 以 content:// 作为开头,这叫做 Scheme,是 ContentUri 结构中的一部分,完整结构如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a0xYP95z-1613113441634)(.\Day17~2021-02-03.assets\image-20210204023540721.png)]

  • Scheme: 是 Android 应用中 URI 的标准开头

  • Content Authority:也称作 内容主机名,指定要使用的 ContentProvider,必须与应用清单文件中 <provider> 标签的 authority 相匹配。

    当中一个 URI 使用与 应用清单文件 <provider> 中相匹配的主机名时,就会使用 <provider> 标签中 name 属性对应的 Provider 类(其实,就是指向了一个数据库)

  • Type of data:指定了要执行操作的数据,一个常用的模式是 将这部分作为 表名,/contacts 即表示访问整个数据表。

为了帮助理解,下面列出几个 来自 Contacts Provider 中不同表的几个 URI 示例:

  1. content://com.android.contacts/contacts
  2. content://com.android.contacts/profile
  3. content://com.android.contacts/photo
  4. content://com.android.contacts/diretories

这几个 URI 都是从同一个 Contacts Provider 中进行调用,因为它们使用了同样的 内容主机名(来自同一数据库),但在结尾列出的表名不同,所有它们可能分别访问了 contacts 表、profile 表、photo 表 或 directories 表,这些表处于同一个数据库中。

那么在 Pets 应用中应该是什么样的呢?以及如何使用 URI 标识表中单个行的数据呢?

  • 访问 Pets 应用中整个 pets 数据表使用 content://com.example.pets/pets

    其中,com.example.pets 为内容主机名,最后的 /pets 表示 pets 数据表

  • 访问表中的单行数据库可以在表名得到后再跟上数字,这个数字为 被访问行的 ID编号,它们看起来可能像这样:

    content://com.android.contacts/contacts/1

    content://com.android.contacts/contacts/2

    content://com.android.contacts/contacts/10

    这三个 URI 分别指向联系人应用中 contacts 数据表的第 1、2、10行的数据。

如果在 宠物应用中查询 pets 表中的所有记录的 URI 为 content://com.example.pets/pets

假如要更新 id 为 5 的这行数据,则 URI 为 content://com.example.pets/pets/5

最后强调,Android 应用中的 URI 一定要以 content:// 作为标准开头。然后是类似 com.example.pets 的内容主机名,它指定了要使用的 Content Provider,这些是在 应用的清单文件的 <provider> 标签中定义的。最后由 /pets/5 指定了要执行操作的数据(可以是整个表 或表中的单个行)

使用哪个 Content URI

我们为宠物应用设计了两种 URI,访问整个表的 content://com.example.pets/pets 和表中单个行的 content://com.example.pets/pets/5

在宠物应用的 CatalogActivity 中我们希望显示 所有的宠物列表,这意味着我们需要查询整个表,则使用以表名 /pets 结尾的 URI。

那么假设要 在 EditorActivity 中显示表中已经存在的某个宠物的信息,就需要从表中查询单个行,即使用含 id 编号的 /pets/5 的 URI,它指向 要显示的宠物信息所在的行。

这里不妨整理一下,列出 CatalogActivity 和 EditorActivity 中所有可能执行的数据操作,并从 A、B 两个选项中选择合适的 URI 类型。

选项A: content://com.example.pets/pets

选项B: content://com.example.pets/pets/1

在 EditorActivity 中:

  • 更新表中 id 为 1 的宠物信息?

    答案:B,因为需要从表中找到 id 为 1 的行,才能对已有的数据进行更新。

  • 删除表中 id 为 1 的宠物信息?

    答案:B,先从表中找到 id 为 1 的行,才能对已有的数据进行删除。

  • 添加一条新的宠物信息?

    答案:A,插入一行新的数据并不需要访问已存在的某行,只需要访问 数据表就足够了。

在 CatalogActivity 中:

  • 添加一只虚拟的宠物信息?

    答案:A,插入一行新的数据并不需要访问已存在的某行,只需要访问 数据表就足够了。

  • 删除所有的宠物信息?

    答案:A,删除所有数据是针对整个数据表的操作,所以选 A。

向 Contract 添加 URI

现在是时候在 Pets 应用中把我们 设计的 URI 使用上了,前边写那么多是因为 设计和使用正确的 URI 对我们从表中获取所需的信息非常重要。现在,来看看如何向 PetContract.java 代码添加 URI。

还记得 URI 的 3 个部分吗?scheme(标准开头)、Content Authority (内容主机名) 和 Type of data(数据类型)。

content://com.example.pets/pets/2 为例,由于其中某些成分是可重复使用的,不会发生变化,我们可以将它们作为常数。

那么现在的问题是存储这些常数的最佳地方是哪里。记得之间将与数据相关的所有常数都存储在了 Contract 类中,所以这也是存储 URI 常数信息的一个理想选择。

首先来看看之前在 AndroidManifest 标签中设置的 ContentProvider 的内容主机名(Content Authority):

 <provider
      android:name=.data.PetProvider”
      android:authorities=”com.example.pets”
      android:exported=false/>

PetContract.java 中,我们将它设置为一个字符串常数,它的值和 AndroidManifest 中的一样:

public static final String CONTENT_AUTHORITY = "com.example.pets";

接下来,将 CONTENT_AUTHORITY 常数与 scheme标准开头 content:// 连接起来,我们将创建常量 BASE_CONTENT_URI 作为基本内容 URI,它将由与 PetsProvider 关联的每一个 URI 共用:

 "content://" + CONTENT_AUTHORITY

要使这个 URI 有用,我们将使用 Uri 类的 parse() 方法,它将 URI 字符串作为输入,然后返回一个 URI 类型的对象。

 public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);

然后是表名 pets ,此常数存储位置将会被附加到基本内容 URI 的每个表的路径。

 public static final String PATH_PETS = "pets";

最后,在 contract 中的每个 Entry 类中,我们为其创建一个完整的 URI 作为常数 CONTENT_URI

Uri.withAppendedPath() 方法将 BASE_CONTENT_URI(包含 标准开头 和内容主机名)附加到 。

 public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, PATH_PETS);

在添加了这些常数后,PetContract.java 类看起来将是 这样的

更改完成前后的差异。

ContentProvider 实现

URI Matcher

我们已经在 PetContact 添加上了用于访问数据库的 URI,那么 整个过程是什么样的呢,假设在 UI 代码中通过 URI 发起了一个请求,PetProvider 接收到这个 URI 后如何与数据源进行交互(在宠物应用中的数据源为 数据库)?

比如在 Pets 应用中的 UI 代码(CatalogActivity),它使用宠物 URI content://com.example.pets/pets 向 ContentResolver 发起查询请求,ContentResolver 会判断这个宠物 URI 具有 内容主机名 com.example.pets 并将这个查询请求发送到 主机名所对应的 .data.PetProvider

PetProvider 接收到查询请求后就得执行相应的操作,为了决定如何处理请求 PetProvider 会使用 Uri Matcher。Uri Matcher 会确定传递给它的 URI 属于哪种类型,然后根据不同的 URI 类型而执行不同的操作,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SBdPW4Ym-1613113441637)(.\Day17~2021-02-03.assets\image-20210205043039929.png)]

所以,一旦 Uri Matcher 接受到用于数据请求的 URI 后,就可以使用这个 URI 来决定将要执行什么样的操作,这时 URI 的使命就完成了。Uri Matcher 已经确切得知道要执行操作的数据表了,并通过与 SQLite 数据库对象直接交互来操作表。

那么,为了区分 Provider 中不同宠物的行为,我们需要列出所有可能的 URI 模式(# 为数字通配符,指表中单个行数据的 id 值):

URI pattern Code Constant name
content://com.example.pets/pets 100 PETS
content://com.example.pets/pets/# 101 PET_ID

现在 Pets 应用中任何情况的宠物 URI 都将遵守以上两种 URI 模式,任何其他的 URI 模式都应该被 PetProvider 识别为无效的 URI,比如 content://com.example.pets/petowner 将无法被识别。为了在提及这两种模式时更方便指明,我为它们分别选了一个唯一的整数代码 100 和 101,也可以是任意数字,只要是唯一的就行了。还为这两种模式分别指定了一个易于记住的名字 PETS 和 PETS_ID,叫任何名字都可以。

那么在 Pets 应用的代码中,就可以为 这两种模式分别创建一个 整型常量 PETS = 100PET_ID = 101,代码为 100 表示针对整个表执行操作,代码为 101 则表示针对表中的单个行数据执行操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fmLL9Axt-1613113441643)(.\Day17~2021-02-03.assets\image-20210205043721496.png)]

所有 URI Matcher 的作用就是帮助 Content Provider 处理 Uri 的内容,以便决定接下来的操作,同时排除掉所有不符合 所在 Content Provider 的 URI 模式。

添加 URI Matcher

要使用 URI Matcher 需要在 Content Provider 中实现两个步骤。

  1. 用 ContentProvider 可接受的 URI 模式设置 URI Matcher,并为每个模式分配一个具有唯一性的 整型代码。

    在 PetProvider 中添加成员变量:

        /** URI pattern code to access whole table pets */
        private static final int PETS = 100;
    
        /** URI pattern code to access whole table pets to access a single row of the table pets */
        private static final int PET_ID = 101;
    
        // Creates a UriMatcher object.
        private static final UriMatcher sUriMatcher;
    
        static {
          
          
            // to access whole table pets
            sUriMatcher.addURI(PetContract.CONTENT_AUTHORITY, PetContract.PATH_PETS, PETS);
    
            // to access a single row of the table pets
            sUriMatcher.addURI(PetContract.CONTENT_AUTHORITY, PetContract.PATH_PETS+"/#", PET_ID);
        }
    
  2. 需要调用 UriMatcher.matcher(Uri) 方法(传入被处理的 Uri),这会返回对应的 URI 模式的整型代码,前提是这个 URI 是有效的模式,如果是无效的 URI 模式也将 返回特殊值 UriMatcher.NO_MATCH 常量 -1

    这步留在 实现 PetProvider 的几个必需方法时,会使用到。

更改完成前后的差异。

必需方法

ContentProvider 实例会处理其他应用发送的请求,从而管理对结构化数据集的访问。所有形式的访问最终都会调用 ContentResolver,后者接着通过调用 ContentProvider 的具体方法来获取访问权限。

抽象类 ContentProvider 定义了六个抽象方法,您必须将其作为具体子类的一部分加以实现。以下所有方法(onCreate() 除外)均由尝试访问内容提供程序的客户端应用调用:

  • query()

    从提供程序中检索数据。使用参数选择要查询的表、要返回的行和列以及结果的排序顺序。将数据作为 Cursor 对象返回。

  • insert()

    在提供程序中插入新行。使用参数选择目标表并获取要使用的列值。返回新插入行的内容 URI。

  • update()

    更新提供程序中的现有行。使用参数选择要更新的表和行,并获取更新后的列值。返回已更新的行数。

  • delete()

    从提供程序中删除行。使用参数选择要删除的表和行。返回已删除的行数。

  • getType()

    返回内容 URI 对应的 MIME 类型。如需了解此方法的更多信息,请参阅实现内容提供程序 MIME 类型部分。

  • onCreate()

    初始化提供程序。创建提供程序后,Android 系统会立即调用此方法。请注意,只有在 ContentResolver 对象尝试访问您的提供程序时,系统才会创建它。

请注意,这些方法与同名的 ContentResolver 方法拥有相同的签名。

您在实现这些方法时应考虑以下事项:

  • 所有这些方法(onCreate() 除外)均可由多个线程同时调用,因此它们必须是线程安全的方法。如需了解有关多线程的更多信息,请参阅进程和线程主题。
  • 避免在 onCreate() 中执行冗长的操作。将初始化任务推迟到实际需要时执行。如需了解有关此方法的更多信息,请参阅实现 onCreate() 方法部分。
  • 尽管您必须实现这些方法,但您的代码只需返回要求的数据类型,而无需执行任何其他操作。例如,您可能想防止其他应用向某些表插入数据。如要实现此目的,您可以忽略 insert() 调用并返回 0。

实现 query() 方法

ContentProvider.query() 方法必须返回 Cursor 对象,如果失败,系统会抛出 Exception。如果您使用 SQLite 数据库作为数据存储,则只需返回由 SQLiteDatabase 类的某个 query() 方法返回的 Cursor。如果查询不匹配任何行,则您应该返回一个 Cursor 实例(其 getCount() 方法返回 0)。只有当查询过程中出现内部错误时,您才应该返回 null

如果您不使用 SQLite 数据库作为数据存储,请使用 Cursor 的某个具体子类。例如,在 MatrixCursor 类实现的游标中,每行都是一个 Object 数组。对于此类,请使用 addRow() 来添加新行。

请记住,Android 系统必须能够跨进程边界传达 Exception。Android 可以为以下异常执行此操作,这些异常可能有助于处理查询错误:

  • IllegalArgumentException(您可以选择在提供程序收到无效的内容 URI 时抛出此异常)
  • NullPointerException

在宠物应用中的 PetProvider 中实现:

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
    
    
        // Get readable database
        SQLiteDatabase db = mDbHelper.getReadableDatabase();

        // This cursor will hold the result of the query
        Cursor cursor;

        // Figure out if the URI matcher can match the URI to a specific code
        switch (sUriMatcher.match(uri)) {
    
    
            case PETS:
                cursor = db.query(PetContract.PetEntry.TABLE_NAME, projection, selection,selectionArgs, null, null, sortOrder);
                break;
            case PET_ID:
                selection = PetContract.PetEntry._ID + "=?";
                selectionArgs = new String[]{
    
    String.valueOf(ContentUris.parseId(uri))};
                cursor = db.query(PetContract.PetEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
                break;
            default:
                throw new IllegalArgumentException("Cannot query unknown  URI "+ uri);
        }

        return cursor;
    }

更改完成前后的差异。

使用 query() 方法

虽然现在已经实现了 PetProvider 的 query() 方法,但在 UI 代码中仍然是 直接访问数据库的,并没有使用到 PetProvider。我们应该使用 ContentResolver 和 PetProvider 传递一个 Uri 来与数据库进行交互。

在 UI 代码 CatalogActivity 中更改 查询所有宠物信息的方法 dispalyDatabaseInfo()

    /**
     * Temporary helper method to display information in the onscreen TextView about the state of
     * the pets database.
     */
    private void displayDatabaseInfo() {
    
    
        // Define a projection that specifies which columns from the database
        // you will actually use after this query.
        String[] projection = {
    
    
                BaseColumns._ID,
                PetEntry.COLUMN_PET_NAME,
                PetEntry.COLUMN_PET_BREED,
                PetEntry.COLUMN_PET_GENDER,
                PetEntry.COLUMN_PET_WEIGHT,
        };

        // Perform a query on the pets table
        Cursor cursor = getContentResolver().query(
                PetEntry.CONTENT_URI,           // The Content Uri: "com.example.pets/pets"
                projection,                    // The array of columns to return (pass null to get all)
                null,                // The columns for the WHERE clause
                null,            // The values for the WHERE clause
                null                  // The sort order
        );

        ………………
    }

更改完成后,为了能更明显的 知道 PetProvider.query() 在什么时候调用,可以在里面加入两行代码:

public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
    
    
    Toast.makeText(getContext(),"PetProvider.query() called.",Toast.LENGTH_SHORT).show();
    Log.i(LOG_TAG,"PetProvider.query() called.");
    
    …………
}

最后运行应用,更改完成前后代码差异对比。

由于每插入一只虚拟宠物都会 调用 dispalyDatabaseInfo() 辅助方法来更新 UI,所以每次都是通过 Provider.query() 方法从数据库获取的最新状态。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yN1CY47p-1613113441646)(.\Day17~2021-02-03.assets\Video_20210207_115847_934.gif)]

实现 insert() 方法

insert() 方法会使用 ContentValues 参数中的值,向相应表中添加新行。如果 ContentValues 参数中未包含列名称,您可能希望在提供程序代码或数据库模式中提供其默认值。

此方法应返回新行的内容 URI。如要构造此方法,请使用 withAppendedId() 向表的内容 URI 追加新行的 _ID(或其他主键)值。

首先,在 PetProvider 类中 由 insert() 方法 运行实际的 插入宠物到数据库的功能,这个功能有一个新的辅助方法 Provider.insertPet() 实现,在 PetProvider.java 中:

    @Nullable
    @Override
    public Uri insert(Uri uri, ContentValues contentValues) {
    
    
        final int match = sUriMatcher.match(uri);
        switch (match) {
    
    
            case PETS:
                return insertPet(uri, contentValues);
            default:
                throw new IllegalArgumentException("Insertion is not supported for " + uri);
        }
    }

    /**
     * Insert a pet into the database with the given content values. Return the new content URI
     * for that specific row in the database.
     */
    private Uri insertPet(Uri uri, ContentValues values) {
    
    

        // TODO: Insert a new pet into the pets database table with the given ContentValues

        // Once we know the ID of the new row in the table,
        // return the new URI with the ID appended to the end of it
        return ContentUris.withAppendedId(uri, id);
    }

由于 insertPet() 方法中有一个 TODO,我们接下来详细看看它。我们已经从 UriMatcher 结果中知道我们处于 PETS case,所以我们需要继续按图的流程往下走,获取一个数据库对象,然后执行插入,最后根据插入结果来决定返回的 URI。

问题就在于与如何才能返回插入成功后的 宠物URI 对象。解决思路如下:

  1. 我们首先获得一个数据库对象。它应该是可读还是可写入数据库呢? 由于我们要通过添加新宠物来编辑数据源,所以我们需要向数据库写入变化。
  2. 然后我们需要进行数据库插入。这个应该是熟悉的,因为已经在 EditorActivity 中使用过直接向 SQLiteDatabase 中插入宠物了。 一旦我们有了数据库对象后,我们可以对它调用 insert() 方法,传入宠物表名称和 ContentValues 对象。返回值是刚创建的新行的 ID,属于 long 数据类型(它可以比 int 数据类型存储更大的数字)。
  3. 根据 ID,我们可以决定数据库操作进行的是否顺利。如果 ID 等于 -1,那我们就知道插入失败了。否则,插入将是成功的。因此,我们在代码中加入此检查。如果插入失败,我们使用 Log.e() 记录错误消息,并返回一个为空值的 URI。这样,当一个类尝试插入宠物,但收到空的 URI 时,它们将知道出现了错误。
  4. 如果插入成功,那么我们可以将行 ID 添加到宠物 URI 的结尾(使用 ContentUris.withAppendedId() 方法),以创建一个特定于新宠物的宠物 URI。

最终根据用上思路 Provider.insertPet() 方法如下:

/**
 * Insert a pet into the database with the given content values. Return the new content URI
 * for that specific row in the database.
 */
private Uri insertPet(Uri uri, ContentValues values) {
    
    

    // Get writeable database
    SQLiteDatabase database = mDbHelper.getWritableDatabase();

    // Insert the new pet with the given values
    long id = database.insert(PetContract.PetEntry.TABLE_NAME, null, values);
    // If the ID is -1, then the insertion failed. Log an error and return null.
    if (id == -1) {
    
    
        Log.e(LOG_TAG, "Failed to insert row for " + uri);
        return null;
    }

    // Return the new URI with the ID (of the newly inserted row) appended at the end
    return ContentUris.withAppendedId(uri, id);
}

代码更改前后差异对比

使用 insert() 方法

现在我们来看此如何使用 UI 代码调用 PetProvider insert() 方法 即 CatalogActivity 和 EditorActivity。在这 2 个活动中,我们将删除对 PetDbHelper 和 SQLiteDatabase 对象的引用,仅使用 URI 与 ContentResolver 交互。

在 CatalogActivity 中,当用户点击“插入虚拟宠物”(Insert Dummy Pet) 菜单项时,我们将使用宠物内容 URI 和 ContentValues 对象(包含关于 TODO 的信息)调用 ContentResolver insert() 方法。这是 insertPet() 方法的更新版本,它在菜单项被点击时从 onOptionsItemSelected() 方法调用。

在 CatalogActivity.java 中:

/**
 * Helper method to insert hardcoded pet data into the database. For debugging purposes only.
 */
private void insertPet() {
    
    
    // Create a ContentValues object where column names are the keys,
    // and Toto's pet attributes are the values.
    ContentValues values = new ContentValues();
    values.put(PetEntry.COLUMN_PET_NAME, "Toto");
    values.put(PetEntry.COLUMN_PET_BREED, "Terrier");
    values.put(PetEntry.COLUMN_PET_GENDER, PetEntry.GENDER_MALE);
    values.put(PetEntry.COLUMN_PET_WEIGHT, 7);

    // Insert a new row for Toto into the provider using the ContentResolver.
    // Use the {@link PetEntry#CONTENT_URI} to indicate that we want to insert
    // into the pets database table.
    // Receive the new content URI that will allow us to access Toto's data in the future.
    Uri newUri = getContentResolver().insert(PetEntry.CONTENT_URI, values);
}

在此文件中,我们也删除对 PetDbHelper(导入语句、全局变量定义和 onCreate() 方法中的初始化)的所有引用。我们也删除了 insertPet() 方法中对 SQLiteDatabase 对象的引用。

在我们进行更多 UI 代码变更之前,请确保应用依然可编译并运行。测试“插入虚拟宠物”(Insert Dummy Pet) 菜单项依然像之前一样正常运行。

接下来,在 EditorActivity 中,我们重复同样的任务,只做轻微变化。在删除对 PetDbHelper 和 SQLiteDatabase 的引用后,我们可以使用宠物内容 URI 和从用户输入字段构建的 ContentValues 对象调用 ContentResolver insert() 方法。SQLiteDatabase insert() 方法和 ContentResolver insert() 方法之间的一个重大差别在于一个返回行 ID,而另一个返回 Uri。

由于 ContentResolver 返回的是 Uri,我们可以修改代码以检查 Uri 是否为空值。然后我们会显示一条 toast 消息,告诉用户插入是否成功。我们使用一般 toast 消息,排除关于行 ID/宠物 URI 的详细信息,因为它们是只有开发人员才会关心的内部细节。以下是完整的 insertPet() 方法,或者你也可以在这里查看整个 EditorActivity 文件。

/**
 * Get user input from editor and save new pet into database.
 */
private void insertPet() {
    
    
    // Read from input fields
    // Use trim to eliminate leading or trailing white space
    String nameString = mNameEditText.getText().toString().trim();
    String breedString = mBreedEditText.getText().toString().trim();
    String weightString = mWeightEditText.getText().toString().trim();
    int weight = Integer.parseInt(weightString);

    // Create a ContentValues object where column names are the keys,
    // and pet attributes from the editor are the values.
    ContentValues values = new ContentValues();
    values.put(PetEntry.COLUMN_PET_NAME, nameString);
    values.put(PetEntry.COLUMN_PET_BREED, breedString);
    values.put(PetEntry.COLUMN_PET_GENDER, mGender);
    values.put(PetEntry.COLUMN_PET_WEIGHT, weight);

    // Insert a new pet into the provider, returning the content URI for the new pet.
    Uri newUri = getContentResolver().insert(PetEntry.CONTENT_URI, values);

    // Show a toast message depending on whether or not the insertion was successful
    if (newUri == null) {
    
    
        // If the new content URI is null, then there was an error with insertion.
        Toast.makeText(this, getString(R.string.editor_insert_pet_failed),
                Toast.LENGTH_SHORT).show();
    } else {
    
    
        // Otherwise, the insertion was successful and we can display a toast.
        Toast.makeText(this, getString(R.string.editor_insert_pet_successful),
                Toast.LENGTH_SHORT).show();
    }
}

确保将用户可见的字符串移到 strings.xml 文件用于本地化目的,在 res/values/strings.xml 中:

<!-- Toast message in editor when new pet has been successfully inserted [CHAR LIMIT=NONE] -->
<string name="editor_insert_pet_successful">Pet saved</string>

<!-- Toast message in editor when new pet has failed to be inserted [CHAR LIMIT=NONE] -->
<string name="editor_insert_pet_failed">Error with saving pet</string>

运行应用并测试,确保创建新宠物是否依然正确运行。如果是,恭喜成功在 内容提供程序 中实现了 insert() 功能,并更新了 UI 代码来调用 Provider 代码!(代码更改前后差异对比

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p5C0fi5h-1613113441649)(.\Day17~2021-02-03.assets\Video_20210208_120608_589.gif)]

数据完整性检查

你可以看到,确定用户输入的宠物数据有效是非常重要的。在我们的上下文中,对数据进行完整性检查(也称为数据验证或输入验证)意味着进行一个快速测试,以在将数据插入数据库前,确保数据在你的合理期望内。

一旦无效数据进入你的数据库,要整理良好和不良数据可就麻烦了。它会让你的数据分析变得困难,因为你观察到的趋势会不可靠。而且,UI 代码会变得极其复杂,因为它必须处理所有这些异常值,而无法对数据做出特定假设。

向 PetProvider insert() 和 update() 方法添加完整性检查

在我们的应用中,进行完整性检查的最佳位置是在 PetProvider 中,在对数据库进行任何更改前执行。特别是,PetProvider 暴露 query()、insert()、update() 和 delete() 方法,对吧? 但是查询数据不需要对数据库进行任何更改,所以无需在此添加任何检查。删除数据也不会添加新数据。但是,插入和更新数据就需要在数据库中插入新数据,所以我们需要对这些 Provider 方法进行完整性检查。 一个有趣的类比就是将 内容提供程序 视为警察,它负责允许或拒绝进入数据库的数据。

内容提供程序 还有另外一个优势。如果没有它,我们就得在插入或更新宠物的 UI 代码中的所有地方复制/粘贴同样的数据验证逻辑。 当复制粘贴操作较多时,难免会引入错误。而且将来的开发人员有可能会调整一个地方的数据验证代码,但意外地忘记了调整其他地方的代码。但是现在,所有的逻辑都可以集中在 PetProvider 文件中,如果需要修改,我们只在一个地方修改即可。

检查 ContentValues 对象中的值

我们将对 insert() 和 update() 方法中传入的 ContentValues 对象的每个值进行完整性检查。由于我们仅实现了 PetProvider insert() 方法,我们主要在此方法中进行数据验证。之后,当实现 update() 方法时,确保也进行数据验证。

第 1 步:确定每种数据的要求

第一步是写下 ContentValues 捆绑包中每个值的要求:名称、品种、性别和体重。例如,不想让空名称进入数据库。

第 2 步:在代码中添加检查来执行这些要求

第二步是获取每项要求,在 PetProvider.insert() 方法中测试它们。我用名称属性向你展示一个示例,我们不希望名称为 null。

我们可以根据键名从 ContentValues 对象中提取一项属性。我们可以使用 ContentValues.getAsString(PetEntry.COLUMN_PET_NAME)提取为名称存储的字符串值,比如它可以是 Tommy。

假设“values”是一个 ContentValues 对象:

String name = values.getAsString(PetEntry.COLUMN_PET_NAME);

你可以根据你感兴趣的属性的数据类型,使用其他 ContentValues 方法,如:getAsInteger()getAsBoolean()。更多可用的 ContentValues 方法请参考官方文档

然后我们可以检查 ContentValues 对象的名称是否为空。如果为空,我们应使用错误消息抛出一个新的 IllegalArgumentException,说“需要为宠物添加名称”(Pet requires a name),而非继续创建新宠物。 这样,调用此 Provider 方法的任何开发人员将知道他们需要更改代码,以为宠物提供一个名称。

(注:如“Android 基础知识:网络”中所介绍,可以抛出异常并停止应用运转,以向开发人员发出信号,说明发生了错误,如果接受错误数据并容纳它会使结果更糟。 理想情况下,调用此方法的 UI 代码会足够智能,向最终用户显示错误来告诉他们在到达应用崩溃点之前提供一个宠物名称。)

PetProvider.java 中:

private Uri insertPet(Uri uri, ContentValues values) {
    
    
    // Check that the name is not null
    String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
    if (name == null) {
    
    
        throw new IllegalArgumentException("Pet requires a name");
    }

名称可以为空的原因有 2 个。要么 Contentvalues 对象明确添加了代码:values.put(PetEntry.COLUMN_PET_NAME, null)。或从一开始键/值对就未添加到 ContentValues 对象。记住,不保证宠物应用中的 ContentValues 对象里有全部 4 个宠物属性。在 UI 代码的某个地方,我们可能不小心忘记了添加某个属性,如名称,而仅向 ContentValues 捆绑包添加了品种、性别和体重。

不管是怎样发生的,PetProvider 仅关心数据不包含空名称,否则就会抛出一个错误。

提示:你可能需要在 PetContract 中编写一个方法,使用 PetContract 中声明的性别常数,确定性别值是否有效。

在 PetProvider.java 中:

private Uri insertPet(Uri uri, ContentValues values) {
    
    
    // Check that the name is not null
    String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
    if (name == null) {
    
    
        throw new IllegalArgumentException("Pet requires a name");
    }

    // TODO: Finish sanity checking the rest of the attributes in ContentValues

    // Get writeable database
    SQLiteDatabase database = mDbHelper.getWritableDatabase();

    // Insert the new pet with the given values
    long id = database.insert(PetEntry.TABLE_NAME, null, values);
    // If the ID is -1, then the insertion failed. Log an error and return null.
    if (id == -1) {
    
    
        Log.e(LOG_TAG, "Failed to insert row for " + uri);
        return null;
    }

    // Return the new URI with the ID (of the newly inserted row) appended at the end
    return ContentUris.withAppendedId(uri, id);
}

解决方法

对于品种 Breed,此字段可以为空,无需在 Provider 中检查该值。对于性别,该字段必须为非空,且必须等于以下有效性别常数中的一个:GENDER_MALEGENDER_FEMALEGENDER_UNKNOWN。 最后,体重(weight)属性略有点麻烦。严格来说,从我们定义 pets 表的方式来看,重量可以为空。我们添加了一个数据库约束,在没有提供重量的情况下可以使用默认值 0。所以我们允许空重量值。但是如果提供了重量值,我们必须确保它大于或等于 0。但不允许负重量。

检查性别

我们可以跳过品种,对性别进行完整性检查。由于性别存储为一个整数,我们使用 ContentValues.getAsInt() 方法并传入性别列键。

PetProvider.insertPet() 方法中:

  Integer gender = values.getAsInteger(PetEntry.COLUMN_PET_GENDER);

如果性别为空或其并非有效性别值中的一个,那么我们就抛出一个 IllegalArgumentException,显示错误消息“请为宠物提供有效的性别”(Pet requires valid gender)。 注意,在 PetEntry.isValidGender(gender) 前加上“!”符号表示该值的相反值。如果 isValidGender() 返回 true,那么在它签名加上“!”符号,返回的值将为 false。 如果 isValidGender()返回的值为 false,那么在它前面添加“!”符号将使返回的值为 true。 我还使用了“||”运算符,因为如果性别为空或无效,那么“if”检查将为 true,我们应抛出一个异常。这个逻辑有点复杂,你可以尝试孤立“if”检查的每个部分,一个一个来,以确保你全部搞清楚。

  if (gender == null || !PetEntry.isValidGender(gender)) {
    
    
      throw new IllegalArgumentException("Pet requires valid gender");
  }

我在 PetContract 的定义了性别常数的 PetEntry 类中定义了 isValidGender() 方法。此方法将整数作为输入,根据整数是否有有效性别(等于 GENDER_MALE、GENDER_FEMALE 或 GENDER_UNKNOWN)返回 true 或 false。 我打算将这个辅助方法放在 PetContract 中,因为我认为它在应用的多个地方可以用到。

在 PetContract.java 文件的 PetEntry 类中:

/**
   * Returns whether or not the given gender is {@link #GENDER_UNKNOWN}, {@link #GENDER_MALE},
   * or {@link #GENDER_FEMALE}.
   */
  public static boolean isValidGender(int gender) {
    
    
      if (gender == GENDER_UNKNOWN || gender == GENDER_MALE || gender == GENDER_FEMALE) {
    
    
          return true;
      }
      return false;
  }

好的,这样我们就可以确保性别值满足我们的要求。

检查体重

要从 ContentValues 对象中提取重量值,我们使用 ContentValues.getAsInteger() 方法,并传入重量作为键/值对中的键。 在 PetProvider.insertPet() 方法中:

// If the weight is provided, check that it's greater than or equal to 0 kg
  Integer weight = values.getAsInteger(PetEntry.COLUMN_PET_WEIGHT);

如果重量为空,没关系,我们可以继续进行插入(数据库会自动插入默认重量 0)。如果重量不为空,而为负值,那么我们需要抛出一个异常,显示消息“请为宠物提供有效的重量”(Pet requires valid weight)。 我们使用“&&”符号表明”“weight != null”和“weight < 0”都必须为 true,才能使整个测试条件的结果为 true,并执行“if”语句中的代码。

  if (weight != null && weight < 0) {
    
    
      throw new IllegalArgumentException("Pet requires valid weight");
  }

如果所有完整性检查都通过了,且值都是合理的,那么我们便可以继续使用代码向数据库中插入宠物。这是此编码任务结尾处的 insertPet() 方法。

    /**
     * Insert a pet into the database with the given content values. Return the new content URI
     * for that specific row in the database.
     */
    private Uri insertPet(Uri uri, ContentValues values) {
    
    
        // Check that the name is not null
        String name = values.getAsString(PetContract.PetEntry.COLUMN_PET_NAME);
        if (null == name) {
    
    
            throw new IllegalArgumentException("Pet requires a name");
        }

        // Check that the gender is valid
        Integer gender = values.getAsInteger(PetContract.PetEntry.COLUMN_PET_GENDER);
        if (null == gender || !PetContract.PetEntry.isValidGender(gender)) {
    
    
            throw new IllegalArgumentException("Pet requires valid gender");
        }

        // If the weight is provided, check that it's greater than or equal to 0 kg
        Integer weight = values.getAsInteger(PetContract.PetEntry.COLUMN_PET_WEIGHT);
        if (null != weight && 0 > weight) {
    
    
            throw new IllegalArgumentException("Pet requires valid weight");
        }

        // No need to check the breed, any value is valid (including null).

        // Get writeable database
        SQLiteDatabase database = mDbHelper.getWritableDatabase();

        // Insert the new pet with the given values
        long id = database.insert(PetContract.PetEntry.TABLE_NAME, null, values);
        // If the ID is -1, then the insertion failed. Log an error and return null.
        if (-1 == id) {
    
    
            Log.e(LOG_TAG, "Failed to insert row for " + uri);
            return null;
        }

        // Return the new URI with the ID (of the newly inserted row) appended at the end
        return ContentUris.withAppendedId(uri, id);
    }

棒极了!相信到此,你已经完全明白了在 Provider 中添加基本检查对确保你的数据库中的数据干净的重要性,并且它将在以后为你省去很多让人头疼的工作。更改完成代码前后对比

实现 update() 方法

update() 方法与 insert() 采用相同的 ContentValues 参数,并且该方法与 delete()ContentProvider.query() 采用相同的 selectionselectionArgs 参数。如此一来,您便可在这些方法之间重复使用代码。

update() 方法概述

请参阅 内容提供程序 update() 的文档,了解此方法的输入和输出。输入参数为 Uri、ContentValues 对象以及 selection 和 selectionArgs。返回值为成功更新的行的编号。

下面是 update() 方法的端到端流程。

在这里插入图片描述

根据来自 UriMatcher 的结果,PETS 和 PET_ID case 均受支持。在 PETS case 中,调用者想要按照 selection 和 selectionArgs 更新 pets 表中的多个行。 在 PET_ID case 中,调用者想要更新特定宠物。要写入数据库中的新值包含在传入方法的 ContentValues 对象中。

在这里插入图片描述

我们可以看到如何返回代表受影响行的整数。

使用案例 1

假设我们想将虚拟宠物条目 Toto 更新为不同的宠物 Milo,一只法国斗牛犬!你可以在 Instagram 上的 @frenchiebutt 查看此货的一些超蠢萌图片。 由于它们都是公的,我们无需更新性别。我们只需在 ContentValues 中包含 3 个属性即可:名称、品种和重量。

update() 方法的输入示例:

URI: content://com.example.android.pets/pets/
ContentValues: name is Milo, breed is French bulldog, weight is 20
Selection: “name=?”
SelectionArgs: { “Toto” }

update() 方法的输入示例:

SQLite statement: UPDATE pets SET name = ‘Milo’, breed=’French bulldog’, weight=20 WHERE name=‘Toto’

结果:

If we started off with 3 Toto’s in our pet table, then a successful update operation would return the number 3 - for 3 rows being updated to have Milo’s attributes.

使用案例 2

假如我们想将单个宠物(比如 Tommy)更新为 Milo。这次,我们不用传入带 selection 和 selectionArgs 的 URI,而是 Tommy 的特定内容 URI(比如 content://com.example.android.pets/pets/5)。 由于它们的性别都为公,我们无需更新性别。我们只需在 ContentValues 中包含 3 个属性:姓名、品种和重量。

update() 方法的示例输入:

URI: content://com.example.android.pets/pets/5
ContentValues: name is Milo, breed is French bulldog, weight is 20

在 update() 方法中:

SQLite statement: UPDATE pets SET name = ‘Milo’, breed=’French bulldog’, weight=20 WHERE _id=5

结果:

A successful update operation would return 1, for one row being updated to have Milo’s attributes (specifically row #5).

update() 方法的代码

现在我们来实现代码。将你的 PetProvider 类中的当前 update() 方法更新为下面提供的方法。并添加 updatePet() 辅助方法。

在 PetProvider.java 中:

@Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
    
    
        switch (sUriMatcher.match(uri)) {
    
    
            case PETS:
                return updatePet(uri, values, selection, selectionArgs);
            case PET_ID:
                // For the PET_ID code, extract out the ID from the URI,
                // so we know which row to update. Selection will be "_id=?" and selection
                // arguments will be a String array containing the actual ID.
                selection = PetContract.PetEntry._ID + "=?";
                selectionArgs = new String[]{
    
    String.valueOf(ContentUris.parseId(uri))};
                return updatePet(uri, values, selection, selectionArgs);
            default:
                throw new IllegalArgumentException("Update is not supported for " + uri);
        }
    }

    /**
     * Update pets in the database with the given content values. Apply the changes to the rows
     * specified in the selection and selection arguments (which could be 0 or 1 or more pets).
     * Return the number of rows that were successfully updated.
     */
    private int updatePet(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    
    
        ………………
    }

你会注意到 PETS 和 PET_ID cases 都调用了updatePet() 方法来执行实际的数据库操作。唯一的区别是在 PET_ID case 中,我们多了 2 行代码,用来手动设置 selection 字符串和 selection arguments 数组根据传入的宠物 URI 指向单个宠物。 与 PetProvider.query() 方法中的逻辑类似,我们将 selection字符串设为“id=?”,而 selectionArgs 为我们关心的行 ID(通过使用 ContentUris.parseId(Uri) 方法从 URI 中抽取 ID)。 这是 update() 方法中发生的主要步骤。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M75LsNdp-1613113441655)(https://video.udacity-data.com/topher/2019/August/5d47a3f5_6wz-tkgqlvafoxskb296mk5wi38c6qsc01g0zf0zaikstxo-bem95u-hdd3on16ysvogn3f6jeqvyquiufa-s0w-586-h-420/6wz-tkgqlvafoxskb296mk5wi38c6qsc01g0zf0zaikstxo-bem95u-hdd3on16ysvogn3f6jeqvyquiufa-s0w-586-h-420)]

Provider.update() 方法会返回受影响的行的编号。如果你尝试使用空 ContentValues 对象调用 update() 方法, Provider 将返回更新的行数为 0。

根据 updatePet() 辅助方法上的注释,你会看到此方法用于执行实际的数据库更新操作。

对 ContentValues 对象中的数据进行完整性检查,由于你在向数据库中插入新数据,确保名称、品种、性别和重量值满足我们在之前练习中列出的要求。

虽然数据要求与 insert() 方法的一样,但是仍然有一个关键差别。对于 insert() 方法,由于要插入的是全新宠物,所有所有属性(品种除外)都应提供。但对于 update() 方法,你的 ContentValues 对象中不需要全部四个属性。你只需要更新一个属性,例如品种。在此情况下,你更新的字段无需在 ContentValues 对象中。这些字段(ContentValues 对象中不包含的)将和之前保持一样。

因为无需提供所有的值,我们建议你在检查值是否合理前,使用 ContentValues.containsKey() 方法来检查键/值对是否存在。

实现数据更新操作

PetProvider.update() 方法中,对每个可能的更新值执行完整性检查。首先,我们使用 ContentValues containsKey() 方法来检查是否存在每个属性。如果存在键,那我们就从其中提取值,然后检查它是否有效。

我们可以换种方式思考此代码变更,即我们在每个宠物属性(来自 insertPet() 方法)的代码块四周包裹一个“if”检查,以先确认属性是存在的。

在 PetProvider.java 中:

private int updatePet(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    
    
    // If the {@link PetEntry#COLUMN_PET_NAME} key is present,
    // check that the name value is not null.
    if (values.containsKey(PetEntry.COLUMN_PET_NAME)) {
    
    
        String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
        if (name == null) {
    
    
            throw new IllegalArgumentException("Pet requires a name");
        }
    }

    // If the {@link PetEntry#COLUMN_PET_GENDER} key is present,
    // check that the gender value is valid.
    if (values.containsKey(PetEntry.COLUMN_PET_GENDER)) {
    
    
        Integer gender = values.getAsInteger(PetEntry.COLUMN_PET_GENDER);
        if (gender == null || !PetEntry.isValidGender(gender)) {
    
    
            throw new IllegalArgumentException("Pet requires valid gender");
        }
    }

    // If the {@link PetEntry#COLUMN_PET_WEIGHT} key is present,
    // check that the weight value is valid.
    if (values.containsKey(PetEntry.COLUMN_PET_WEIGHT)) {
    
    
        // Check that the weight is greater than or equal to 0 kg
        Integer weight = values.getAsInteger(PetEntry.COLUMN_PET_WEIGHT);
        if (weight != null && weight < 0) {
    
    
            throw new IllegalArgumentException("Pet requires valid weight");
        }
    }

    // No need to check the breed, any value is valid (including null).

这也是对 ContentValues 对象进行快速检查的好机会。如果它里面没有键/值对,那么仅返回 0 行受影响。如果没有可以更新的新值,则无需对数据库执行操作,而且每个数据库操作都会占用设备上的一些内存资源。

// If there are no values to update, then don't try to update the database
if (values.size() == 0) {
    
    
    return 0;
}

如果我们实际想要对数据库执行一些更改,那么从 PetDbHelper 获取可写入的数据库(因为我们在对数据源执行编辑)。 一旦我们有了 SQLiteDatabase 对象,我们对它调用 update() 并传入表名、新的 ContentValues、selection 和 selectionArgs。SQLiteDatabase update() 方法的返回值为受影响的行的编号。所以我们可以直接返回它。

在 PetProvider.java 中:

/**
 * Update pets in the database with the given content values. Apply the changes to the rows
 * specified in the selection and selection arguments (which could be 0 or 1 or more pets).
 * Return the number of rows that were successfully updated.
 */
private int updatePet(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    
    
  	………………

    // Otherwise, get writeable database to update the data
    SQLiteDatabase database = mDbHelper.getWritableDatabase();

    // Returns the number of database rows affected by the update statement
    return database.update(PetEntry.TABLE_NAME, values, selection, selectionArgs);
}

运行应用以确保它依然可编译。我们将在下一篇博文中连接 UI 的更新功能时真正测试此代码是否正确实现了。

代码更改完成前后差异

实现 delete() 方法

delete() 方法无需从您的数据存储中实际删除行。如果您将同步适配器与提供程序一起使用,则应考虑为已删除的行添加“删除”标志,而不是完全移除行。同步适配器可以检查是否存在已删除的行,并将这些行从服务器中移除,然后再将其从提供程序中删除。

Delete() 方法概述

从 内容提供程序 delete() 方面的文档中,我们了解到此方法有 3 个输入:uri、selection 和 selectionArgs。返回值是成功删除的行的编号。 delete() 方法官方文档

这里是 delete() 方法端到端的流程图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BccHTu4B-1613113441656)(https://video.udacity-data.com/topher/2019/August/5d47a58e_ghiq0mr5h4x-d-5-bu25a0btqtzbygd8mhzliqd2vgyk-vj12raajg6a7udyjp6pwdse8wcekb7qvdk1xc-s0w-764-h-334/ghiq0mr5h4x-d-5-bu25a0btqtzbygd8mhzliqd2vgyk-vj12raajg6a7udyjp6pwdse8wcekb7qvdk1xc-s0w-764-h-334)]

UriMatcher 帮助确定执行这两种 case 中的哪一个:PETS 还是 PET_ID case。在 PETS case 中,调用者想要根据 selection 和 selectionArgs 删除 pets 表中的多个行。在 PET_ID case 中,调用者想要删除特定宠物。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N9iX3j8n-1613113441658)(https://video.udacity-data.com/topher/2019/August/5d479e21_fihb4eaibhcirlnert5ogkpqlwpokqx4ujmlpb-tg75rklos-k-gtnwlcyhvuidrklfokrdwffspa8ehleun-s0w-766-h-426/fihb4eaibhcirlnert5ogkpqlwpokqx4ujmlpb-tg75rklos-k-gtnwlcyhvuidrklfokrdwffspa8ehleun-s0w-766-h-426)]

上面的图显示了返回值为代表删除行编号的整数。

使用案例 1 - 删除多行

假设收容所开展了一个“收养花斑猫”的活动,其中所有的花斑猫都被收养了。这意味着我们需要从 pets 表中删除所有品种为花斑猫的动物。

delete() 方法示例输入

URI: content://com.example.android.pets/pets
Selection: “breed=?”
SelectionArgs: { “Calico” }

在 delete() 方法中:

SQLite statement: DELETE pets WHERE breed= ‘Calico’

结果:

成功的删除操作会返回 pets 表中最初花斑猫的数量。例如,如果收容所中最初有 10 只花斑猫,删除操作后我们会获得 10 个行的编号。  

使用案例 2 - 删除 1 只宠物

例如,法国斗牛犬 Milo(@frenchiebutt on instagram)非常可爱,有一个家庭来收养了它!这意味着我们需要从带收养宠物表中将它删除。

delete() 方法输入示例:

URI: content://com.example.android.pets/pets/5
Selection: “name=?”
SelectionArgs: { “Milo” }

在 delete() 方法中:

SQLite statement: DELETE pets WHERE _id=5

结果:

成功的删除操作将返回 1,因为一个行被删除。## delete() 方法的代码

编码实现

delete() 方法和其他方法看起来很像。我首先抓取数据库的可写入版本,然后匹配 URI。我有两个 case,一个是删除所有宠物,一个是删除单个宠物。

如果未给定任何一个的 URI,将抛出异常,就像其他方法中一样。到此,我的四个 CRUD 方法就完成了。

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
    
    
    // Get writeable database
    SQLiteDatabase database = mDbHelper.getWritableDatabase();

    final int match = sUriMatcher.match(uri);
    switch (match) {
    
    
        case PETS:
            // Delete all rows that match the selection and selection args
            return database.delete(PetEntry.TABLE_NAME, selection, selectionArgs);
        case PET_ID:
            // Delete a single row given by the ID in the URI
            selection = PetEntry._ID + "=?";
            selectionArgs = new String[] {
    
     String.valueOf(ContentUris.parseId(uri)) };
            return database.delete(PetEntry.TABLE_NAME, selection, selectionArgs);
        default:
            throw new IllegalArgumentException("Deletion is not supported for " + uri);
    }
}

delete 方法的使用留在 下一篇博文中 实现。更改完成前后的差异。

实现 getType() 方法

你可能注意到在 PetProvider 中,还有一个我们需要重载的方法:getType(Uri uri) 方法。此方法的用途是返回描述输入 URI 中存储的数据类型的字符串。该字符串为 MIME 类型,也称为内容类型。

此功能比较重要的一个使用案例是当你随数据字段上的 URI 集发送 intent 时,Android 系统会检查此 URI 的 MIME 类型,确定设备上的哪个应用组件最适合处理你的请求。(如果 URI 恰巧为内容 URI,那么系统会检查相应的 内容提供程序,使用 getType() 方法获取 MIME 类型。) 在此文章“构建 Intent”(Building an Intent) 部分的“数据”(Data) 标题下了解详情。

Android 文档中是这样描述 内容提供程序 getType() 方法的:

实现此 [方法] 来处理给定 URI 的 MIME 数据类型请求。返回的 MIME 类型对个单个记录应以“vnd.android.cursor.item” 开头,多个项应以“vnd.android.cursor.dir/”开头。

联系人应用 MIME 类型示例

定义一开始看起来可能有点不太清楚,我们通过一个示例来说明。ContactsProvider 可以处理许多不同类型的数据:联系人、照片、电话号码、邮箱地址等……每种数据的 MIME 类型不同。了解 getType() 方法如何在 ContactsProvider 中实现。

对于常见的数据类型,如图像,已经有广泛使用的 MIME 类型字符串惯例:“image/jpeg”或“image/png”(或其他图像文件扩展名)。然后,对于特定于应用的数据,你可以自定义 MIME 类型。

联系人应用为单个联系人定义了自定义 MIME 类型, 作为 ContactsContract 中的常数值。然后,如果 URI 指代单个联系人,getType() 方法将返回此 MIME 类型。注意,自定义 MIME 类型以“vnd.android.cursor.item”开头(如之前的定义所述),因为它为单个记录。在此情况中,“单个记录”指数据库表中的单个行。

  /**
   * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
   * person.
   */
  public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact";

如果 URI 指代整个联系人列表,那联系人应用将使用不同的自定义 MIME 类型。注意,此情况下的自定义 MIME 类型以“vnd.android.cursor.dir”开头(其中“dir”为目录的缩写),因为 URI 可以指向多个记录。此处多个记录指数据库中的多个行或多个联系人。

  /**
   * The MIME type of {@link #CONTENT_URI} providing a directory of
   * people.
   */
  public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact";

MIME 类型的另一种解释

接下来,你阅读此 StackOverflow 帖子的回复,其中很好地解释了 MIME 类型对于 内容提供程序 的重要性并提供了示例。 (维基百科上的这篇文章也更详细地解释了 MIME 类型。)

定义自定义宠物 MIME 类型

在我们的应用中,基本上有两种类型的 URI。第一种 URI 为 content://com.example.android.pets/pets/,指代整个 pets 表。它代表整个宠物列表。用 MIME 类型来说,这称为数据目录。 第二种 URI 是 content://com.example.android.pets/pets/#,它代表单个宠物。用 MIME 类型来说,单个数据行即单个数据项。

  content://com.example.android.pets/pets → Returns directory MIME type
  content://com.example.android.pets/pets/# → Returns item MIME type

由于 MIME 类型要遵循特定格式,以下是我们应用中数据的 MIME 类型。MIME 类型字符串按约定以“vnd.android.cursor…”开头,后面跟宠物内容主机名,以及数据路径。

目录 MIME 类型: vnd.android.cursor.dir/com.example.android.pet/pets

项 MIME 类型: vnd.android.cursor.item/com.example.android.pet/pets

**第 1 步:**在 PetContract 中声明 MIME 类型常数

要在我们的 PetProvider 中实现这个行为,首先应该在 PetContract 文件中在 PetEntry 内声明代表 MIME 类型的常数。一个细微的差别就在于 cursor 后的词:diritem

PetEntry.CONTENT_LIST_TYPE vnd.android.cursor.dir/com.example.android.pet/pets

PetEntry.CONTENT_ITEM_TYPE vnd.android.cursor.item/com.example.android.pet/pets

将以下代码添加到你的应用。你可以在文件中定义了宠物内容 URI 的位置后面插入这些常数。

在 PetContract.java 中:

      public static final class PetEntry implements BaseColumns {
    
    /**
           * The MIME type of the {@link #CONTENT_URI} for a list of pets.
           */
          public static final String CONTENT_LIST_TYPE =
                  ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PETS;

          /**
           * The MIME type of the {@link #CONTENT_URI} for a single pet.
           */
          public static final String CONTENT_ITEM_TYPE =
                  ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PETS;

你会注意到,我们在使用 ContentResolver 类中定义的常数:CURSOR_DIR_BASE_TYPE(它映射到常数“vnd.android.cursor.dir”)和 CURSOR_ITEM_BASE_TYPE(映射到常数“vnd.android.cursor.item”)。 因此,将 ContentResolver 类的此额外 import 语句添加到 PetContract 的顶部(若尚未自动导入)。

在 PetContract.java 中:

  import android.content.ContentResolver;

**第 2 步:**实现 内容提供程序 getType() 方法

接下来是对 getType() 方法的实际实现,在这里必须对每一个 Uri 返回正确的 MIME 类型。将此版本的 getType() 方法替换为你的 PetProvider 中当前存在的空白方法。

UriMatcher PETS case → Return MIME type PetEntry.CONTENT_LIST_TYPE

UriMatcher PET_ID case → Return MIME type PetEntry.CONTENT_ITEM_TYPE

在 PetProvider.java 中:

       @Override
      public String getType(Uri uri) {
    
    
          final int match = sUriMatcher.match(uri);
          switch (match) {
    
    
              case PETS:
                  return PetEntry.CONTENT_LIST_TYPE;
              case PET_ID:
                  return PetEntry.CONTENT_ITEM_TYPE;
              default:
                  throw new IllegalStateException("Unknown URI " + uri + " with match " + match);
          }
      }

到此,构建你的首个 内容提供程序 就完成了,祝贺你!拍拍你自己的备或与附近的朋友或任何人击掌欢呼吧!

代码更改前后差异,最后确保用于能够正常运行()

总结

在本次学习中,修改了应用并为其添加了 Content Provider,这使得 UI 代码将不会直接和数据库进行交互,而是通过 Content Provider 来调用。

所有的数据库操作都被封装在 ContentProvider 当中,这就可以在 操作数据的同时 进行数据验证,并抛出异常。

对于 ContentProvider 的学习就基本告一段落了,不得不说 这是 Android 应用的一个重点。

尽管现在的 宠物应用中 PetProvider 能够 为我们对宠物数据进行 增删改查,但是目前宠物应用只能够 添加一条宠物信息 和 查询所有宠物并显示(尽管 界面不怎么好看),其余的 删除、更新 宠物的功能都将在 下次的学习当中实现。

参考

Content Provider 概述

Content Provider 基础知识

UriMatcher | Android Developers

创建 Content Provider

Content Providers in Android with Example - GeeksforGeeks

Content Resolver 文档

What is the mimeType attribute in used for?

猜你喜欢

转载自blog.csdn.net/weixin_45075891/article/details/113794946