Batch operation of ContentProvider

Keywords: Android Database github Java

Overview

ContentProvider is one of the core components of android system. It encapsulates the data access interface. Its underlying data is usually stored in the database or in the cloud.

In most cases, we don't really need ContentProvider. If our application interacts with other applications, we can use the SQLite database directly. (Look before you want to use ContentProvider.) Official documents Then decide whether it really needs to be. But since we have some ORM open source libraries, we seldom even operate the database by ourselves.

Content Provider and Content Resolver provide us with a basic interface to meet most of our needs. But when the amount of data processed is large, we can choose to call the corresponding function of ContentResolver several times or use batch operation. Of course, the latter will perform better.

bulkInsert

If only the bulk insertion of a single table is involved, we can directly use bulkInsert (Uri uri, Content Values [] values) for bulk insertion.

ContentProviderOperation

In order to make batch update, insert and delete data more convenient, android system introduced ContentProviderOperation
Class.
ContentProvider Operations is recommended in official development documents for the following reasons:
1. All operations are performed in one transaction to ensure data integrity.
2. Because batch operations are executed in one transaction, only one transaction needs to be opened and closed, which is better than opening and closing multiple transactions many times.
3. Using batch operations and multiple single operations reduces context switching between applications and content provider s, which also improves application performance, reduces CPU usage time and, of course, reduces power consumption.

ContentProviderOperation.Builder

To create a ContentProviderOperation object, you need to use ContentProviderOperation.Builder Class to obtain a Builder object by calling the following static functions:

function purpose
newInsert Create a Builder for insertion (support multiple tables)
newUpdate Create a Builder to perform update operations
newDelete Create a Builder for deleting operations
newAssertQuery It can be understood as a breakpoint query, that is, query whether there is qualified data, if not, will throw an OperationApplicationException exception.

This Buidler object uses the well-known Builder design pattern. Because the functions of the Builder object return to themselves, the final ContentProvider Operation object can be generated through a series of function chain calls.

    /*
     * Prepares the batch operation for inserting a new raw contact and its data. Even if
     * the Contacts Provider does not have any data for this person, you can't add a Contact,
     * only a raw contact. The Contacts Provider will then add a Contact automatically.
     */

     // Creates a new array of ContentProviderOperation objects.
    ArrayList<ContentProviderOperation> ops =
            new ArrayList<ContentProviderOperation>();

    /*
     * Creates a new raw contact with its account type (server type) and account name
     * (user's account). Remember that the display name is not stored in this row, but in a
     * StructuredName data row. No other data is required.
     */
    ContentProviderOperation.Builder op =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
            .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, mSelectedAccount.getType())
            .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, mSelectedAccount.getName());

    // Builds the operation and adds it to the array of operations
    ops.add(op.build());

Of course, you can also use familiar ones. ContentValues Object, the corresponding function is withValues(values).

The Core Function of Builder

Introduction to Builder Object Core Functions API):

function purpose
withSelection (String selection, String[] selectionArgs) Specify the data conditions that need to be operated on. Can only be used for update, delete, or assert.
withSelectionBackReference(int selectionArgIndex, int previousResult) Add a "backward reference" as the query condition. The value of the selectionArgIndex position previously specified by withSelection(String, String []) will be overwritten. Can only be used for update, delete, or assert.
withValue (String key, Object value) Defining a column of data values is similar to adding a data to ConetentValues. It can only be used for insert, update, or assert.
withValues (ContentValues values) Define multi-column data values. Can only be used for insert, update, or assert
withValueBackReference(String key, int previousResult) Add a Back Reference. Use the value in the Back Reference to set the value of the specified "key" column. The so-called Back Reference is actually the ContentProvider Result returned after the completion of the ContentProvider Operation in a set of Operations. If it is an insert operation, it will use the ID in the uri returned by the ContentProvider Operation, and if it is an update or assert, it will use the returned count. This value overrides the previous value set by withValues(ContentValues). It can only be used for insert, update, or assert.
withValueBackReferences(ContentValues backReferences) Add a Back Reference. Use ContentValues to complete multiple withValueBackReference operations. The key and value in ContentValues correspond to the index of column name and previous Result, referring to the parameters of withValueBackReference. Value is added as a String. This value overrides the previous value set by withValues(ContentValues). It can only be used for insert, update, or assert.
withExpectedCount(int count) Verify the number of rows affected, and if they are different from count, an OperationApplication Exception is thrown. It can only be used for update, delete, or assert operations.

Reference to Backward Reference Some Notes on the Official Website

Finally passed ContentResolver applyBatch() function to apply batch operations:

try {
   getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
} catch (RemoteException e) {
   // do s.th.
} catch (OperationApplicationException e) {
   // do s.th.
}

Working principle

Starting with the build() method of ContentProviderOperation.Builder, you can see that a new ContentProviderOperation() has been constructed.

        /** Create a ContentProviderOperation from this {@link Builder}. */
        public ContentProviderOperation build() {
            if (mType == TYPE_UPDATE) {
                if ((mValues == null || mValues.size() == 0)
                        && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)) {
                    throw new IllegalArgumentException("Empty values");
                }
            }
            if (mType == TYPE_ASSERT) {
                if ((mValues == null || mValues.size() == 0)
                        && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)
                        && (mExpectedCount == null)) {
                    throw new IllegalArgumentException("Empty values");
                }
            }
            return new ContentProviderOperation(this);
        }

The batch operation starts with getContentResolver().applyBatch(ContactsContract.AUTHORITY, operationList), and the call eventually goes to ContentProvider.applyBatch (), which does two things:
1. A ContentProviderResult [] array is defined. The size of the array is equal to the size of operations. ContentProvider Result is used to save the execution results of each ContentProvider Operation. There are two types of ContentProvider Result: a specific "uri" and the number of "count" rows affected by this operation, which will eventually be used in "backward references".
2. Traverse the operations and call the corresponding ContentProviderOperation.apply operation, and return the results to the corresponding ContentProviderResult [] array to save.

    public @NonNull ContentProviderResult[] applyBatch(
            @NonNull ArrayList<ContentProviderOperation> operations)
                    throws OperationApplicationException {
        final int numOperations = operations.size();
        final ContentProviderResult[] results = new ContentProviderResult[numOperations];
        for (int i = 0; i < numOperations; i++) {
            results[i] = operations.get(i).apply(this, results, i);
        }
        return results;
    }

In the ContentProviderOperation. application method, there are several important steps:
1. Call resolveValueBackReferences() first, and process the ContentValue of "backward reference".
2. Call resolveSelection ArgsBackReferences to process the query parameters of "backward reference".
3. If it is an insert operation, the provider.insert is called directly, and the uri returned by the insert is assigned to the new ContentProvider Result (newUri).
4. Similarly, if it is delelte, update, it also calls provider.delete, provider.update directly, and assigns the return numRows to the new ContentProvider Result (numRows)
5. Relative to AssertQuery, call provider.query directly to compare the queried data with the expected Values, and return the new ContentProvider Result (numRows) of the corresponding rows if the same; otherwise, report Exception.
6. If mExpected Count is not empty (indicating that with Expected Count (int count) is set), it will be compared with numRows to determine whether the expected value is the same as the actual operation value and report OperationApplication Exception if it is different.

    public ContentProviderResult apply(ContentProvider provider, ContentProviderResult[] backRefs,
            int numBackRefs) throws OperationApplicationException {
        ContentValues values = resolveValueBackReferences(backRefs, numBackRefs);
        String[] selectionArgs =
                resolveSelectionArgsBackReferences(backRefs, numBackRefs);

        if (mType == TYPE_INSERT) {
            Uri newUri = provider.insert(mUri, values);
            if (newUri == null) {
                throw new OperationApplicationException("insert failed");
            }
            return new ContentProviderResult(newUri);
        }

        int numRows;
        if (mType == TYPE_DELETE) {
            numRows = provider.delete(mUri, mSelection, selectionArgs);
        } else if (mType == TYPE_UPDATE) {
            numRows = provider.update(mUri, values, mSelection, selectionArgs);
        } else if (mType == TYPE_ASSERT) {
            // Assert that all rows match expected values
            String[] projection =  null;
            if (values != null) {
                // Build projection map from expected values
                final ArrayList<String> projectionList = new ArrayList<String>();
                for (Map.Entry<String, Object> entry : values.valueSet()) {
                    projectionList.add(entry.getKey());
                }
                projection = projectionList.toArray(new String[projectionList.size()]);
            }
            final Cursor cursor = provider.query(mUri, projection, mSelection, selectionArgs, null);
            try {
                numRows = cursor.getCount();
                if (projection != null) {
                    while (cursor.moveToNext()) {
                        for (int i = 0; i < projection.length; i++) {
                            final String cursorValue = cursor.getString(i);
                            final String expectedValue = values.getAsString(projection[i]);
                            if (!TextUtils.equals(cursorValue, expectedValue)) {
                                // Throw exception when expected values don't match
                                Log.e(TAG, this.toString());
                                throw new OperationApplicationException("Found value " + cursorValue
                                        + " when expected " + expectedValue + " for column "
                                        + projection[i]);
                            }
                        }
                    }
                }
            } finally {
                cursor.close();
            }
        } else {
            Log.e(TAG, this.toString());
            throw new IllegalStateException("bad type, " + mType);
        }

        if (mExpectedCount != null && mExpectedCount != numRows) {
            Log.e(TAG, this.toString());
            throw new OperationApplicationException("wrong number of rows: " + numRows);
        }

        return new ContentProviderResult(numRows);
    }

In the resolveValueBackReferences method, it is first judged whether mValuesBackReferences is empty or not. If it is empty, mValues is returned directly to mValues. mValues is the ContentValue object assembled by the values filled in by the withValue or withValues method, such as the values to be updated or inserted. If mValuesBackReferences! = null (using withValueBackReference or withValueBackReferences), you need to deal with the value of "backward reference", which is actually to find the value of the ContentProvider Result returned by the completed ContentProvider Operation and bind it to the corresponding key (column name). Looking for "backward references" is implemented in the backRefToValue function, and keep looking down.

The resolveSelection ArgsBackReferences function also serves a similar purpose.

    public ContentValues resolveValueBackReferences(
            ContentProviderResult[] backRefs, int numBackRefs) {
        if (mValuesBackReferences == null) {
            return mValues;
        }
        final ContentValues values;
        if (mValues == null) {
            values = new ContentValues();
        } else {
            values = new ContentValues(mValues);
        }
        for (Map.Entry<String, Object> entry : mValuesBackReferences.valueSet()) {
            String key = entry.getKey();
            Integer backRefIndex = mValuesBackReferences.getAsInteger(key);
            if (backRefIndex == null) {
                Log.e(TAG, this.toString());
                throw new IllegalArgumentException("values backref " + key + " is not an integer");
            }
            values.put(key, backRefToValue(backRefs, numBackRefs, backRefIndex));
        }
        return values;
    }

Two cases are handled in backRefToValue, if uri is not empty in ContentProvider Result, the corresponding ID of uri is returned; if it is empty, the count value is returned. So, as you can see from the apply function above, insert corresponds to ID; delete, update, assertQuery returns count.

    private long backRefToValue(ContentProviderResult[] backRefs, int numBackRefs,
            Integer backRefIndex) {
        if (backRefIndex >= numBackRefs) {
            Log.e(TAG, this.toString());
            throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex
                    + " but there are only " + numBackRefs + " back refs");
        }
        ContentProviderResult backRef = backRefs[backRefIndex];
        long backRefValue;
        if (backRef.uri != null) {
            backRefValue = ContentUris.parseId(backRef.uri);
        } else {
            backRefValue = backRef.count;
        }
        return backRefValue;
    }

Use transaction

Reference resources MediaProvider.java Implementation, using transactions in applyBatch:

  @NonNull
    @Override
    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws OperationApplicationException {
        SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        db.beginTransaction();
        try {
            ContentProviderResult[] results = super.applyBatch(operations);
            db.setTransactionSuccessful();
            return results;
        } finally {
            db.endTransaction();
        }
    }

Reference resources

Introduction to batch operation of ContentProvider in Android Developer - Contact Provider
Android Contact Provider Synchronization Adapter Gibhub sample code
Answer to withValueBackReference on Stackoverflow
Android's ContentProviderOperation: "withBackReference" explained
Android uses ContentProvider Operation to add contacts

Posted by nyi8083 on Mon, 25 Mar 2019 18:24:28 -0700