The goal: refresh database from XML data
The process:
Start transaction
Delete all existing rows from the tables
Per each main element of parsed XML insert row into main table and get PK
Per each child of the main element insert record into 2nd table providing FK from the previous step
Commit transaction
Pretty standard stuff as far as db operations. The problem is that CRUD operations are not done within ContentProvider but rather using ContentResolver so the insert for example looks like resolver.insert(CONTENT_URI, contentValues). The ContentResolver API doesn't seem to have anything pertained to transaction and I cannot use bulkInsert since I'm inserting in 2 tables intermittently (plus I want to have delete inside the transaction as well).
I was thinking of registering my customized ContentProvider as listener by using registerContentObserver but since ContentResolver#acquireProvider methods are hidden how do I obtain the right reference?
Am I out of luck?
I've seen that in the source code of Google I/O application, they override ContentProvider's applyBatch() method and use transactions inside of it. So, you create a batch of ContentProviderOperation s and then call getContentResolver().applyBatch(uri_authority, batch).
I'm planning to use this approach to see how it works. I'm curious if anyone else has tried it.
It is possible to do transaction based multi table inserts rather cleanly since Android 2.1 by using ContentProviderOperation, as mentioned by kaciula.
When you build the ContentProviderOperation object, you can call .withValueBackReference(fieldName, refNr). When the operation is applied using applyBatch, the result is that the ContentValues object that is supplied with the insert() call will have an integer injected. The integer will be keyed with the fieldName String, and its value is retrieved from the ContentProviderResult of a previously applied ContentProviderOperation, indexed by refNr.
Please refer to the code sample below. In the sample, a row is inserted in table1, and the resulting ID (in this case "1") is then used as a value when inserting the row in table 2. For brevity, the ContentProvider is not connected to a database. In the ContentProvider, there are printouts where it would be suitable to add the transaction handling.
public class BatchTestActivity extends Activity {
/** Called when the activity is first created. */
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
ArrayList<ContentProviderOperation> list = new
ArrayList<ContentProviderOperation>();
list.add(ContentProviderOperation.
newInsert(BatchContentProvider.FIRST_URI).build());
ContentValues cv = new ContentValues();
cv.put("name", "second_name");
cv.put("refId", 23);
// In this example, "refId" in the contentValues will be overwritten by
// the result from the first insert operation, indexed by 0
list.add(ContentProviderOperation.
newInsert(BatchContentProvider.SECOND_URI).
withValues(cv).withValueBackReference("refId", 0).build());
try {
getContentResolver().applyBatch(
BatchContentProvider.AUTHORITY, list);
} catch (RemoteException e) {
e.printStackTrace();
} catch (OperationApplicationException e) {
e.printStackTrace();
}
}
}
public class BatchContentProvider extends ContentProvider {
private static final String SCHEME = "content://";
public static final String AUTHORITY = "com.test.batch";
public static final Uri FIRST_URI =
Uri.parse(SCHEME + AUTHORITY + "/" + "table1");
public static final Uri SECOND_URI =
Uri.parse(SCHEME + AUTHORITY + "/" + "table2");
public ContentProviderResult[] applyBatch(
ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
System.out.println("starting transaction");
ContentProviderResult[] result;
try {
result = super.applyBatch(operations);
} catch (OperationApplicationException e) {
System.out.println("aborting transaction");
throw e;
}
System.out.println("ending transaction");
return result;
}
public Uri insert(Uri uri, ContentValues values) {
// this printout will have a proper value when
// the second operation is applied
System.out.println("" + values);
return ContentUris.withAppendedId(uri, 1);
}
// other overrides omitted for brevity
}
All right - so this does not dingle aimlessly: the only way I can think of is to code startTransaction and endTransaction as URL-based query requests. Something like ContentResolver.query(START_TRANSACTION, null, null, null, null). Then in ContentProvider#query based on the registered URL call start or end transaction
You can get the implementation of the content provider object itself (if in the same process, hint: you can control the provider's process with multiprocess="true" or process="" http://developer.android.com/guide/topics/manifest/provider-element.html) using ContentProviderClient.getLocalContentProvider () which can be casted to your provider implementation which can provide extra functionality like a reset() that closes and deletes the database and you can also return a custom Transaction class instance with save() and close() methods.
public class Transaction {
protected Transaction (SQLiteDatabase database) {
this.database = database;
database.beginTransaction ();
}
public void save () {
this.database.setTransactionSuccessful ();
}
public void close () {
this.database.endTransaction ();
}
private SQLiteDatabase database;
}
public Transaction createTransaction () {
return new Transaction (this.dbHelper.getWritableDatabase ());
}
Then:
ContentProviderClient client = getContentResolver ().acquireContentProviderClient (Contract.authorityLocal);
Transaction tx = ((LocalContentProvider) client.getLocalContentProvider ()).createTransaction ();
Related
The problem
Say that I want to insert 1000 elements on a table using a ContentProvider. I'd normally use bulkInsert for that.
But what if I want the IDs of the successful insert operations? I can't get that information through the bulkInsert method, since it only returns a long.
So I wonder, is there a way to do a bulk insert and get the newly inserted IDs?
What I'm doing now
The solution that came up on my mind was to either get the last "N" inserted rows and get the IDs after the bulkInsert, or to make a query based on a column of the inserted rows (Let's say, column 'name').
Either way works, but do I really need to take this extra step? It feels a little redundant...
Example of my bulkInsert:
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
switch (sUriMatcher.match(uri)) {
case BLAH: {
db.beginTransaction();
int rCount = 0;
for (ContentValues value : values) {
long _id = db.insert(SaiteContract.BlahEntry.TABLE_NAME, null, value);
if (_id != -1) {
rCount++;
}
}
db.setTransactionSuccessful();
db.endTransaction();
Context context = getContext();
if (context != null) {
context.getContentResolver().notifyChange(uri,null);
}
return rCount;
}
// Other tables
default:
return super.bulkInsert(uri,values);
}
}
The contract of bulkInsert is to return the number of affected rows, so I wouldn't recommend changing that. If you need to make an arbitrary call, you can use ContentResolver.call(), which returns a Bundle. For example, in your provider implementation:
#Override
public Bundle call(String method, String args, Bundle extras) {
if (method.equals("customInsert")) {
ContentValues[] values = extras.getParcelableArray("contentValues")
// do bulk insert and return list of ids in a new Bundle
}
}
Then simply call
Bundle result = getContentResolver().call(...);
List<Long> ids = result.getLongArray("ids");
Im using the ActiveAndroid library and I have read the entire information (very minimalist and insufficient unfortunately)
There is no mention whether the .save() operation is executed syncrhonously.
If it is asynchronous, how do I "listen" for it to end before proceeding?
http://www.activeandroid.com/ - this is the documentation I read
If you have a look at the source code of the Model class, you'll see that the save method does not do any thread handling:
public final Long save() {
final SQLiteDatabase db = Cache.openDatabase();
final ContentValues values = new ContentValues();
for (Field field : mTableInfo.getFields()) {
/* ... */
}
if (mId == null) {
mId = db.insert(mTableInfo.getTableName(), null, values);
}
else {
db.update(mTableInfo.getTableName(), values, idName+"=" + mId, null);
}
Cache.getContext().getContentResolver()
.notifyChange(ContentProvider.createUri(mTableInfo.getType(), mId), null);
return mId;
}
Saving thus occurs synchronously.
This is my first Application with database, I hope that someone can help me to understand this problem. I have this insert method:
public long insertData(String name, int password){
....
contentValues.put(KEY_NAME, name);
contentValues.put(KEY_PASSWORD, password);
return db.insert(DBHelper.TABle_NAME, null, contentValues);
}
I can insert few data with this method, but what about if I have thousands of rows? how can I insert all these data into database? where can I write all these data, in extra class or what?
As others have said, you'll need to do some sort of iteration.
Efficiency can be gained by performing a bulk transaction. Here's an example:
public int bulkInsert(#NonNull ContentValues[] values) {
int insertCount = 0;
SQLiteDatabase db = mSqlHelper.getWritableDatabase();
try {
db.beginTransaction();
for (ContentValues value : values) {
if (db.insertOrThrow(tableName, null, value) == -1) {
throw new Exception("Unknown error while inserting entry in database.");
}
insertCount++;
}
db.setTransactionSuccessful();
} catch (Exception e) {
Log.e(LOG_TAG, "An error occurred while bulk-inserting database entries.\n" + e.getMessage(), e);
} finally {
db.endTransaction();
}
return insertCount;
}
There is no 'bulk load' facility that I'm aware of.
You'd just have to spin through the list, and insert the items.
You might want to think about why you're potentially trying to insert thousands of items into a database on a hardware-limited device like a phone or a tablet.
Might it be better to put the data on a server, and create an API that you can use to load data (for display) by pages?
you can do it the same way, that you do with few data, you only need to catch the thousands rows to insert into your database using your method, you can use asyntask, or a service to do that
You can use the same method to insert any amount of records, whether it's 1 or 1,000. Use a loop to call your insert method and add your records to your database. Consider putting your database executions in an AsyncTask to prevent your UI thread from hanging.
Your data can come from anywhere, as long as it's formatted to fit your function parameters String, int
I have seen pattern C in the Google IO presentation and I am very anxious to implement this pattern. However, I do really like the ORMLite library and would like to use this library in my application as well.
When I say Google IO presentation I mean this one: https://www.youtube.com/watch?v=xHXn3Kg2IQE by Virgil Dobjanschi.
Now I have been searching a lot for an implementation that shows me how to use ORMLite in conjunction with Contentproviders.
Now my problem here is that the ORMLite DAO is conflicting with the Contentprovider. They essentially do the same and are a pain to integrate into each other. (Using Ormlite in Conjunction with Android's Content Provider others discussing this and agreeing upon this claim.)
A few libraries have implemented ORMLite into the contentprovider API pattern, one example is: https://github.com/blandware/android-atleap
However, underwater they still revert the model to ContentValues (simple types).
Android - Using Dao Pattern with contentProvider
This question is similair to my situation but 3 years ago and I'm suggesting an alternate solution below.
#jcwenger's answer is very useful, but I was wondering if anything has changed in the past 3 years. I'm facing the same issue and perhaps now since ORMLite has matured, it's more rewarding to use ORMLite?
My colleague next to me really, really wants to use ORMLite since he doesn't want to have to write any mapping himself. I know of the existance of the atleap and Android-OrmLiteContentProvider projects. These only provide a cursor to the activity and my colleague want to have lists of models or a single model. Can this be achieved?
My colleague suggests writing my own implementation of the Cursor, SyncAdapter? and Contentprovider (has to be done regardless) to work with models. However can the same functionality still be achieved with lists etc? Passing events to the activity to contentobservers etc?
Is this viable?
Edit
We'll most likely use the contentproviders privately. We do not need to expose these contentproviders. However the advantages that contentproviders provide are great. How else could I notify my GUI to update when the data has changed?
I also have to display data from multiple tables (joins and other data, not contained in the same table) in one activity and download images etc.
So since I couldn't find a proper answer, this is how I solved it after a while of trying:
public class CardProvider extends ContentProvider {
private InternalDatabase dbhelper;
private RuntimeExceptionDao<Card, UUID> cardDao;
/**
* Content authority for this provider.
*/
private static final String AUTHORITY = CardUris.CONTENT_AUTHORITY;
// The constants below represent individual URI routes, as IDs. Every URI pattern recognized by
// this ContentProvider is defined using sUriMatcher.addURI(), and associated with one of these
// IDs.
//
// When a incoming URI is run through sUriMatcher, it will be tested against the defined
// URI patterns, and the corresponding route ID will be returned.
/**
* URI ID for route: /cards
*/
public static final int ROUTE_CARDS = 1;
/**
* URI ID for route: /cards/{ID}
*/
public static final int ROUTE_CARDS_ID = 2;
/**
* UriMatcher, used to decode incoming URIs.
*/
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(AUTHORITY, "cards", ROUTE_CARDS);
sUriMatcher.addURI(AUTHORITY, "cards/*", ROUTE_CARDS_ID);
}
#Override
public int delete(Uri arg0, String arg1, String[] arg2) {
// TODO Auto-generated method stub
return 0;
}
#Override
public String getType(Uri uri) {
final int match = sUriMatcher.match(uri);
switch (match) {
case ROUTE_CARDS:
return CardUris.CONTENT_CARDS;
case ROUTE_CARDS_ID:
return CardUris.CONTENT_ITEM_CARD;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
#Override
public Uri insert(Uri arg0, ContentValues arg1) {
// TODO Auto-generated method stub
return null;
}
#Override
public boolean onCreate() {
dbhelper = OpenHelperManager.getHelper(getContext(), InternalDatabase.class);
cardDao = dbhelper.getRuntimeExceptionDao(Card.class);
return true;
}
#Override
public Cursor query(Uri uri, String[] arg1, String arg2, String[] arg3,
String arg4) {
int uriMatch = sUriMatcher.match(uri);
switch (uriMatch) {
case ROUTE_CARDS_ID:
/*String id = uri.getLastPathSegment();
Card card = null;
try {
card = cardDao.queryBuilder().where().eq(Entry.ID_FIELD_NAME, id).queryForFirst();
} catch (SQLException e) {
e.printStackTrace();
}*/
//return null;
case ROUTE_CARDS:
// Return all known entries.
// Note: Notification URI must be manually set here for loaders to correctly
// register ContentObservers.
// build your query
QueryBuilder<Card, UUID> qb = cardDao.queryBuilder();
// when you are done, prepare your query and build an iterator
CloseableIterator<Card> iterator = null;
Cursor cursor = null;
try {
//qb.query();
iterator = cardDao.iterator(qb.where().eq("relevant", 1).and().eq("removed", false).prepare());
// get the raw results which can be cast under Android
AndroidDatabaseResults results =
(AndroidDatabaseResults)iterator.getRawResults();
cursor = results.getRawCursor();
} catch (SQLException e) {
e.printStackTrace();
} finally {
//iterator.closeQuietly();
}
cursor.setNotificationUri(this.getContext().getContentResolver(), uri);
return cursor;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
#Override
public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
// TODO Auto-generated method stub
return 0;
}
}
You could probably give purpose to the insert, update and delete methods, but the dao does this too and is what I am using.
I am working on a fuel use application which will run on Android 1.6 onwards. The bundled SQLite on v1.6 doesn't do foreign keys, so I've had to handle it manually. So far, I have done this using an Android transaction:
public static long addFuelUp(String registrationNumber, String date)
{
SQLiteDatabase db = uKMpgData.getReadableDatabase();
long result = -1;
ContentValues values = new ContentValues();
Cursor vehicleCursor = VehicleDataProvider.getVehicle(registrationNumber);
if(vehicleCursor.moveToNext())
{
Cursor fuelUpsCursor = getFuelUps(registrationNumber, date);
if(!fuelUpsCursor.moveToNext())
{
db.beginTransaction();
try
{
values.put(REGISTRATION_NO_COLUMN, registrationNumber.replace(" ", ""));
values.put(DATE_TIME_COLUMN, date);
result = db.insertOrThrow(FUEL_USE_TABLE_NAME, null, values);
db.setTransactionSuccessful();
}
catch(SQLException e)
{
Log.d("addFuelUp", e.getMessage());
}
finally
{
db.endTransaction();
vehicleCursor.close();
fuelUpsCursor.close();
}
}
}
return result;
}
I.e. fuel data cannot be entered unless there is a matching vehicle registration number in the database.
My question is, is there a better way to do this? I'm not a database expert, but I know you can set up triggers to enforce rules - are triggers more suited to handle constraints?
Cheers,
Barry
Triggers would be a good solution to this problem.
In fact there is an automated way to generate triggers for simulating foreign keys. SQLite for PC provides a utility called "genfkey" which can examine an existing database which uses foreign keys and outputs the corresponding triggers.