Skip to content
This repository was archived by the owner on Feb 11, 2022. It is now read-only.

Commit e1287a9

Browse files
committed
Merge pull request #50 from novoda/bulk_insert_notify_once
Bulk insert should notify observers only once
2 parents 7cfcba6 + b57d0d7 commit e1287a9

17 files changed

Lines changed: 442 additions & 40 deletions

File tree

core/src/main/java/novoda/lib/sqliteprovider/provider/SQLiteContentProvider.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@
1616

1717
package novoda.lib.sqliteprovider.provider;
1818

19-
import android.content.*;
20-
import android.database.sqlite.*;
19+
import android.content.ContentProvider;
20+
import android.content.ContentProviderOperation;
21+
import android.content.ContentProviderResult;
22+
import android.content.ContentValues;
23+
import android.content.Context;
24+
import android.content.OperationApplicationException;
25+
import android.database.sqlite.SQLiteDatabase;
26+
import android.database.sqlite.SQLiteOpenHelper;
27+
import android.database.sqlite.SQLiteTransactionListener;
2128
import android.net.Uri;
2229

2330
import java.util.ArrayList;
@@ -56,7 +63,12 @@ public boolean onCreate() {
5663
* transaction.
5764
*/
5865
protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
59-
String[] selectionArgs);
66+
String[] selectionArgs);
67+
68+
/**
69+
* The equivalent of the {@link #bulkInsert} method, but invoked within a transaction.
70+
*/
71+
protected abstract int bulkInsertInTransaction(Uri uri, ContentValues[] values);
6072

6173
/**
6274
* The equivalent of the {@link #delete} method, but invoked within a
@@ -107,12 +119,9 @@ public int bulkInsert(Uri uri, ContentValues[] values) {
107119
SQLiteDatabase mDb = mOpenHelper.getWritableDatabase();
108120
mDb.beginTransactionWithListener(this);
109121
try {
110-
for (int i = 0; i < numValues; i++) {
111-
Uri result = insertInTransaction(uri, values[i]);
112-
if (result != null) {
113-
mNotifyChange = true;
114-
}
115-
mDb.yieldIfContendedSafely();
122+
int rowsCreated = bulkInsertInTransaction(uri, values);
123+
if (rowsCreated != 0) {
124+
mNotifyChange = true;
116125
}
117126
mDb.setTransactionSuccessful();
118127
} finally {

core/src/main/java/novoda/lib/sqliteprovider/provider/SQLiteContentProviderImpl.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,28 @@ protected SQLiteOpenHelper getDatabaseHelper(Context context) {
6363

6464
@Override
6565
protected Uri insertInTransaction(Uri uri, ContentValues values) {
66+
Uri insertUri = insertSilently(uri, values);
67+
notifyUriChange(uri);
68+
return insertUri;
69+
}
70+
71+
@Override
72+
protected int bulkInsertInTransaction(Uri uri, ContentValues[] values) {
73+
int rowsCreated = 0;
74+
for (ContentValues value : values) {
75+
Uri insertUri = insertSilently(uri, value);
76+
if (insertUri != null) {
77+
rowsCreated++;
78+
}
79+
getWritableDatabase().yieldIfContendedSafely();
80+
}
81+
notifyUriChange(uri);
82+
return rowsCreated;
83+
}
84+
85+
private Uri insertSilently(Uri uri, ContentValues values) {
6686
long rowId = helper.insert(uri, values);
67-
Uri newUri = ContentUris.withAppendedId(uri, rowId);
68-
notifyUriChange(newUri);
69-
return newUri;
87+
return ContentUris.withAppendedId(uri, rowId);
7088
}
7189

7290
@Override
@@ -127,9 +145,9 @@ public Cursor query(Uri uri, String[] projection, String selection, String[] sel
127145
Map<String, String> autoproj = null;
128146

129147
if (expands.size() > 0) {
130-
builder.addInnerJoin(expands.toArray(new String[] {}));
148+
builder.addInnerJoin(expands.toArray(new String[]{}));
131149
ExtendedSQLiteOpenHelper extendedHelper = (ExtendedSQLiteOpenHelper) getDatabaseHelper();
132-
autoproj = extendedHelper.getProjectionMap(tableName.toString(), expands.toArray(new String[] {}));
150+
autoproj = extendedHelper.getProjectionMap(tableName.toString(), expands.toArray(new String[]{}));
133151
builder.setProjectionMap(autoproj);
134152
}
135153

core/src/test/java/novoda/lib/sqliteprovider/provider/SQLiteProviderLocalTest.java

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
import novoda.lib.sqliteprovider.sqlite.ExtendedSQLiteQueryBuilder;
2626
import novoda.lib.sqliteprovider.util.RoboRunner;
2727

28+
import static org.hamcrest.CoreMatchers.is;
2829
import static org.junit.Assert.assertThat;
2930
import static org.junit.matchers.JUnitMatchers.hasItems;
3031
import static org.mockito.Matchers.anyObject;
3132
import static org.mockito.Matchers.anyString;
3233
import static org.mockito.Matchers.eq;
34+
import static org.mockito.Mockito.any;
3335
import static org.mockito.Mockito.*;
3436

3537
@RunWith(RoboRunner.class)
@@ -38,9 +40,14 @@ public class SQLiteProviderLocalTest {
3840

3941
private SQLiteProviderImpl provider;
4042

41-
@Mock private SQLiteDatabase db;
42-
@Mock private ExtendedSQLiteQueryBuilder builder;
43-
@Mock private Cursor mockCursor;
43+
@Mock
44+
private SQLiteDatabase db;
45+
@Mock
46+
private ExtendedSQLiteQueryBuilder builder;
47+
@Mock
48+
private Cursor mockCursor;
49+
50+
private int notifyChangeCounter;
4451

4552
@Before
4653
public void init() {
@@ -191,8 +198,70 @@ public void testExpand() {
191198
// verify(builder).setTables("table INNER JOIN childs ON table.child_id=childs._id");
192199
}
193200

201+
@Test
202+
public void testBulkInsertInsertsCorrectly() {
203+
when(db.insert(anyString(), anyString(), (ContentValues) anyObject())).thenReturn(2L);
204+
int bulkSize = 100;
205+
ContentValues[] bulkToInsert = createContentValuesArray(bulkSize);
206+
207+
bulkInsert("test.com/table1", bulkToInsert);
208+
209+
verify(db, times(bulkSize)).insert(eq("table1"), anyString(), (ContentValues) anyObject());
210+
}
211+
212+
@Test
213+
public void testBulkInsertNotifiesOnlyOnce() {
214+
when(db.insert(anyString(), anyString(), (ContentValues) anyObject())).thenReturn(2L);
215+
int bulkSize = 100;
216+
ContentValues[] bulkToInsert = createContentValuesArray(bulkSize);
217+
218+
bulkInsert("test.com/table1", bulkToInsert);
219+
220+
assertThat(notifyChangeCounter, is(1));
221+
}
222+
223+
@Test
224+
public void testInsertNotifiesAsManyChangesAsInserts() {
225+
when(db.insert(anyString(), anyString(), (ContentValues) anyObject())).thenReturn(2L);
226+
int insertSize = 100;
227+
ContentValues[] inserts = createContentValuesArray(insertSize);
228+
229+
for (ContentValues insert : inserts) {
230+
insert("test.com/table1", insert);
231+
}
232+
233+
assertThat(notifyChangeCounter, is(insertSize));
234+
}
235+
236+
@Test
237+
public void testUpdateNotifiesAsManyChangesAsUpdates() {
238+
when(db.update(anyString(), (ContentValues) anyObject(), anyString(), any(String[].class))).thenReturn(1);
239+
int updateSize = 100;
240+
ContentValues[] updates = createContentValuesArray(updateSize);
241+
242+
for (ContentValues update : updates) {
243+
update("test.com/table1", update, null, null);
244+
}
245+
246+
assertThat(notifyChangeCounter, is(updateSize));
247+
}
248+
249+
@Test
250+
public void testDeleteNotifiesAsManyChangesAsDeletes() {
251+
when(db.delete(anyString(), anyString(), (String[]) anyObject())).thenReturn(0);
252+
int deleteSize = 100;
253+
254+
for (int i = 0; i < deleteSize; i++) {
255+
delete("test.com/table1", null, null);
256+
}
257+
258+
assertThat(notifyChangeCounter, is(deleteSize));
259+
260+
}
261+
194262
@Implements(ContentUris.class)
195263
static class ShadowContentUris {
264+
196265
@SuppressWarnings("unused")
197266
@Implementation
198267
public static Uri withAppendedId(Uri uri, long id) {
@@ -212,10 +281,24 @@ private void insert(String uri, ContentValues cv) {
212281
provider.insert(Uri.parse("content://" + uri), cv);
213282
}
214283

284+
private void bulkInsert(String uri, ContentValues[] cv) {
285+
provider.bulkInsert(Uri.parse("content://" + uri), cv);
286+
}
287+
215288
private void query(String uri) {
216289
provider.query(Uri.parse("content://" + uri), null, null, null, null);
217290
}
218291

292+
private ContentValues[] createContentValuesArray(int size) {
293+
ContentValues[] bulkToInsert = new ContentValues[size];
294+
ContentValues contentValues = new ContentValues();
295+
contentValues.put("test", "test");
296+
for (int i = 0; i < size; i++) {
297+
bulkToInsert[i] = contentValues;
298+
}
299+
return bulkToInsert;
300+
}
301+
219302
public class SQLiteProviderImpl extends SQLiteContentProviderImpl {
220303
@Override
221304
protected SQLiteDatabase getReadableDatabase() {
@@ -235,7 +318,7 @@ protected ExtendedSQLiteQueryBuilder getSQLiteQueryBuilder() {
235318
@Override
236319
protected SQLiteOpenHelper getDatabaseHelper(Context context) {
237320
try {
238-
return new ExtendedSQLiteOpenHelper(getContext()){
321+
return new ExtendedSQLiteOpenHelper(getContext()) {
239322
@Override
240323
public void onCreate(SQLiteDatabase db) {
241324
// dont do a migrate
@@ -259,6 +342,7 @@ public synchronized SQLiteDatabase getWritableDatabase() {
259342

260343
@Override
261344
public void notifyUriChange(Uri uri) {
345+
notifyChangeCounter++;
262346
}
263347
}
264348
}

demo-extended/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
<activity
2626
android:name=".ui.AddFireworkActivity"
2727
android:label="@string/activity_label_add_new_firework" />
28+
<activity
29+
android:name=".ui.AddBulkFireworksActivity"
30+
android:label="@string/activity_label_add_bulk_fireworks" />
2831
<activity
2932
android:name=".ui.FindFireworkWithPkActivity"
3033
android:label="@string/activity_label_find_firework" />

demo-extended/src/main/java/com/novoda/sqliteprovider/demo/domain/UseCaseFactory.java

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,30 @@ private UseCaseFactory() {
1010
}
1111

1212
public enum UseCase {
13-
ADD, ONE_TO_MANY, PRIMARY_KEY_LOOKUP, SELECT_ALL, DISTINCT, LIMIT, GROUP_HAVING;
13+
ADD, BULK_ADD, ONE_TO_MANY, PRIMARY_KEY_LOOKUP, SELECT_ALL, DISTINCT, LIMIT, GROUP_HAVING;
1414
}
1515

1616
public static UseCaseInfo getInfo(UseCase useCase) {
1717
switch (useCase) {
18-
case ADD:
19-
return createUseCaseInfo(FireworkUriConstants.ADD_FIREWORK, RawSql.INSERT_FIREWORK);
20-
case ONE_TO_MANY:
21-
return createUseCaseInfo(FireworkUriConstants.ONE_TO_MANY_SEARCH, RawSql.SELECT_USING_SHOP_FOREIGN_KEY);
22-
case PRIMARY_KEY_LOOKUP:
23-
return createUseCaseInfo(FireworkUriConstants.PRIMARY_KEY_SEARCH, RawSql.SELECT_USING_PRIMARY_KEY);
24-
case SELECT_ALL:
25-
return createUseCaseInfo(FireworkUriConstants.VIEW_ALL_SEARCH, RawSql.SELECT_ALL);
26-
case DISTINCT:
27-
return createUseCaseInfo(FireworkUriConstants.UNIQUE_SEARCH, RawSql.DISTINCT);
28-
case LIMIT:
29-
return createUseCaseInfo(FireworkUriConstants.LIMIT_3, RawSql.LIMIT);
30-
case GROUP_HAVING:
31-
return createUseCaseInfo(FireworkUriConstants.GROUP_BY_SEARCH, RawSql.GROUP_BY);
32-
default:
33-
Log.e("UseCase " + useCase.toString() + " not found, returning null safe case.");
34-
return UseCaseInfo.getNullSafe();
18+
case ADD:
19+
return createUseCaseInfo(FireworkUriConstants.ADD_FIREWORK, RawSql.INSERT_FIREWORK);
20+
case BULK_ADD:
21+
return createUseCaseInfo(FireworkUriConstants.ADD_FIREWORK, RawSql.BULK_INSERT_FIREWORKS);
22+
case ONE_TO_MANY:
23+
return createUseCaseInfo(FireworkUriConstants.ONE_TO_MANY_SEARCH, RawSql.SELECT_USING_SHOP_FOREIGN_KEY);
24+
case PRIMARY_KEY_LOOKUP:
25+
return createUseCaseInfo(FireworkUriConstants.PRIMARY_KEY_SEARCH, RawSql.SELECT_USING_PRIMARY_KEY);
26+
case SELECT_ALL:
27+
return createUseCaseInfo(FireworkUriConstants.VIEW_ALL_SEARCH, RawSql.SELECT_ALL);
28+
case DISTINCT:
29+
return createUseCaseInfo(FireworkUriConstants.UNIQUE_SEARCH, RawSql.DISTINCT);
30+
case LIMIT:
31+
return createUseCaseInfo(FireworkUriConstants.LIMIT_3, RawSql.LIMIT);
32+
case GROUP_HAVING:
33+
return createUseCaseInfo(FireworkUriConstants.GROUP_BY_SEARCH, RawSql.GROUP_BY);
34+
default:
35+
Log.e("UseCase " + useCase.toString() + " not found, returning null safe case.");
36+
return UseCaseInfo.getNullSafe();
3537
}
3638
}
3739

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.novoda.sqliteprovider.demo.loader;
2+
3+
import android.content.Context;
4+
import android.support.v4.content.AsyncTaskLoader;
5+
6+
import com.novoda.sqliteprovider.demo.domain.Firework;
7+
import com.novoda.sqliteprovider.demo.persistance.FireworkWriter;
8+
9+
import java.util.List;
10+
11+
public class FireworksBulkSaver extends AsyncTaskLoader<List<Firework>> {
12+
13+
private final FireworkWriter writer;
14+
private final List<Firework> fireworks;
15+
public static final int LOADER_ID = 123;
16+
17+
public FireworksBulkSaver(Context context, FireworkWriter writer, List<Firework> fireworks) {
18+
super(context);
19+
this.writer = writer;
20+
this.fireworks = fireworks;
21+
forceLoad();
22+
}
23+
24+
@Override
25+
public List<Firework> loadInBackground() {
26+
writer.saveFireworks(fireworks);
27+
return fireworks;
28+
}
29+
30+
}

demo-extended/src/main/java/com/novoda/sqliteprovider/demo/persistance/DatabaseConstants.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,10 @@ public static class RawSql {
3838
public static final String LIMIT = "SELECT * FROM firework LIMIT 3;";
3939

4040
public static final String DISTINCT = "SELECT DISTINCT name, ftype, color, noise, price FROM firework;";
41+
public static final String BULK_INSERT_FIREWORKS = "BEGIN TRANSACTION\n" +
42+
"FOR Times DO\n" +
43+
"INSERT INTO firework " + "(name, color, noise, ftype, price, " +
44+
"shop_id) VALUES (Na, Co, No, Ft, Pr, Sh);\n" +
45+
"END TRANSACTION";
4146
}
4247
}

demo-extended/src/main/java/com/novoda/sqliteprovider/demo/persistance/DatabaseWriter.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ public void saveDataToFireworksTable(ContentValues values) {
1818
saveDataToTable(DatabaseConstants.TBL_FIREWORKS, values);
1919
}
2020

21+
public void bulkSaveDataToFireworksTable(ContentValues[] values) {
22+
bulkSaveDataToTable(DatabaseConstants.TBL_FIREWORKS, values);
23+
}
24+
25+
private void bulkSaveDataToTable(String table, ContentValues[] values) {
26+
Uri uri = createUri(table);
27+
contentResolver.bulkInsert(uri, values);
28+
}
29+
2130
private void saveDataToTable(String table, ContentValues values) {
2231
Uri uri = createUri(table);
2332
contentResolver.insert(uri, values);

demo-extended/src/main/java/com/novoda/sqliteprovider/demo/persistance/FireworkWriter.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
import com.novoda.sqliteprovider.demo.domain.Firework;
66

7+
import java.util.ArrayList;
8+
import java.util.List;
9+
710
import static com.novoda.sqliteprovider.demo.persistance.DatabaseConstants.Fireworks.*;
811

912
public class FireworkWriter {
@@ -15,15 +18,29 @@ public FireworkWriter(DatabaseWriter databaseWriter) {
1518
}
1619

1720
public void saveFirework(Firework firework) {
21+
ContentValues values = createContentValuesFrom(firework);
22+
databaseWriter.saveDataToFireworksTable(values);
23+
}
24+
25+
public void saveFireworks(List<Firework> fireworks) {
26+
List<ContentValues> valuesArray = new ArrayList<>();
27+
for (Firework firework : fireworks) {
28+
ContentValues values = createContentValuesFrom(firework);
29+
valuesArray.add(values);
30+
}
31+
32+
databaseWriter.bulkSaveDataToFireworksTable(valuesArray.toArray(new ContentValues[fireworks.size()]));
33+
}
34+
35+
private ContentValues createContentValuesFrom(Firework firework) {
1836
ContentValues values = new ContentValues();
1937
values.put(COL_NAME, firework.getName());
2038
values.put(COL_COLOR, firework.getColor());
2139
values.put(COL_NOISE, firework.getNoise());
2240
values.put(COL_TYPE, firework.getType());
2341
values.put(COL_PRICE, firework.getPrice());
2442
values.put(COL_SHOP, 1);
25-
26-
databaseWriter.saveDataToFireworksTable(values);
43+
return values;
2744
}
2845

2946
}

0 commit comments

Comments
 (0)