文章目錄
- 概述
- ContentProvider
- 為何使用它?
- ContentProvider 原理
- 新建 PetProvier
- PetProvider 操作
- Content URI
- 設計寵物 Content URI
- 使用哪個 Content URI
- 向 Contract 添加 URI
- ContentProvider 實作
- URI Matcher
- 添加 URI Matcher
- 必需方法
- 實作 query() 方法
- 使用 query() 方法
- 實作 insert() 方法
- 使用 insert() 方法
- 資料完整性檢查
- 實作 update() 方法
- 實作 delete() 方法
- 實作 getType() 方法
- 總結
- 參考
概述
實際上,在上一篇 安卓學習日志 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)]](https://img.uj5u.com/2021/02/13/224294131259292.png)
而實際實作時,則需要使用自定義的 PetProvider(繼承自 ContentProvider),因為 ContentProvider 是一個抽象類,PetProvider 應作為 com.example.pets.data java包中一個新的 Java 檔案,繼承自 ContentProvider,因此它需要實作五個方法 insert、query、update、delete、 getType 和 onCreate,前四個方法分別對應 資料庫操作中的 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 都有各自的 insert、query、update、delete 方法,只是各自接受的引數不同而已,類似下表:
| 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 交流時基本需要告訴它兩件事:
- 執行什么操作(
insert、query、update、delete) - 被操作的資料(整個資料表 或者 表中的某行)
其中被操作的資料就要使用 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)]](https://img.uj5u.com/2021/02/13/224294131259293.png)
-
Scheme: 是 Android 應用中 URI 的標準開頭
-
Content Authority:也稱作 內容主機名,指定要使用的 ContentProvider,必須與應用清單檔案中
<provider>標簽的authority相匹配,當中一個 URI 使用與 應用清單檔案
<provider>中相匹配的主機名時,就會使用<provider>標簽中name屬性對應的 Provider 類(其實,就是指向了一個資料庫) -
Type of data:指定了要執行操作的資料,一個常用的模式是 將這部分作為 表名,
/contacts即表示訪問整個資料表,
為了幫助理解,下面列出幾個 來自 Contacts Provider 中不同表的幾個 URI 示例:
- content://com.android.contacts/contacts
- content://com.android.contacts/profile
- content://com.android.contacts/photo
- 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)]](https://img.uj5u.com/2021/02/13/224294131259294.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 = 100 和 PET_ID = 101,代碼為 100 表示針對整個表執行操作,代碼為 101 則表示針對表中的單個行資料執行操作,
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fmLL9Axt-1613113441643)(.\Day17~2021-02-03.assets\image-20210205043721496.png)]](https://img.uj5u.com/2021/02/13/224294131259295.png)
所有 URI Matcher 的作用就是幫助 Content Provider 處理 Uri 的內容,以便決定接下來的操作,同時排除掉所有不符合 所在 Content Provider 的 URI 模式,
添加 URI Matcher
要使用 URI Matcher 需要在 Content Provider 中實作兩個步驟,
-
用 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); } -
需要呼叫
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)]](https://img.uj5u.com/2021/02/13/224294131259296.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 物件,解決思路如下:
- 我們首先獲得一個資料庫物件,它應該是可讀還是可寫入資料庫呢? 由于我們要通過添加新寵物來編輯資料源,所以我們需要向資料庫寫入變化,
- 然后我們需要進行資料庫插入,這個應該是熟悉的,因為已經在 EditorActivity 中使用過直接向 SQLiteDatabase 中插入寵物了, 一旦我們有了資料庫物件后,我們可以對它呼叫 insert() 方法,傳入寵物表名稱和 ContentValues 物件,回傳值是剛創建的新行的 ID,屬于 long 資料型別(它可以比 int 資料型別存盤更大的數字),
- 根據 ID,我們可以決定資料庫操作進行的是否順利,如果 ID 等于 -1,那我們就知道插入失敗了,否則,插入將是成功的,因此,我們在代碼中加入此檢查,如果插入失敗,我們使用 Log.e() 記錄錯誤訊息,并回傳一個為空值的 URI,這樣,當一個類嘗試插入寵物,但收到空的 URI 時,它們將知道出現了錯誤,
- 如果插入成功,那么我們可以將行 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)]](https://img.uj5u.com/2021/02/13/224294131259297.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_MALE、GENDER_FEMALE 或 GENDER_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() 采用相同的 selection 和 selectionArgs 引數,如此一來,您便可在這些方法之間重復使用代碼,
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)]](https://img.uj5u.com/2021/02/13/2242941312592910.png)
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)]](https://img.uj5u.com/2021/02/13/2242941312592911.png)
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)]](https://img.uj5u.com/2021/02/13/2242941312592912.png)
上面的圖顯示了回傳值為代表洗掉行編號的整數,
使用案例 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 后的詞:dir 或 item,
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?
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/259263.html
標籤:其他
上一篇:c++讀入方式歸納
