Slide 1

Slide 1 text

Inside the room Effie Barak (@CodingChick)

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Talk Outline: 1. Intro 2. Threading in Room 3. Triggers 4. Transactions 5. Large queries

Slide 4

Slide 4 text

Intro to SQLite

Slide 5

Slide 5 text

Embedded SQL database engine

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

Intro to Android and Android x APIs

Slide 12

Slide 12 text

Android package android.database.sqlite SQLiteStatement SQLiteCursor SQLiteDatabase SQLiteOpenHelper SQLiteQuery

Slide 13

Slide 13 text

Android X androidx.sqlite.db SupportSQLiteDatabase SupportSQLiteOpenHelper SupportSQLiteQuery

Slide 14

Slide 14 text

/** * A database abstraction which removes the framework dependency and allows swapping underlying * sql versions. It mimics the behavior of {@link android.database.sqlite.SQLiteDatabase} */ public interface SupportSQLiteDatabase extends Closeable {

Slide 15

Slide 15 text

Android X androidx.sqlite.db.framework FrameworkSQLiteDatabase FrameworkSQLiteOpenHelper FrameworkSQLiteStatement

Slide 16

Slide 16 text

/** * Delegates all calls to an implementation of {@link SQLiteDatabase}. */ class FrameworkSQLiteDatabase implements SupportSQLiteDatabase {

Slide 17

Slide 17 text

private final SQLiteDatabase mDelegate; @Override public void beginTransaction() { mDelegate.beginTransaction(); } @Override public void beginTransactionNonExclusive() { mDelegate.beginTransactionNonExclusive(); }

Slide 18

Slide 18 text

Intro to Room Architecture

Slide 19

Slide 19 text

Isolated modules 4 room-common 4 room-compiler 4 room-runtime 4 room-ktx 4 room-rxjava2

Slide 20

Slide 20 text

Threads, processes etc

Slide 21

Slide 21 text

Locking in SQLite

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

Types of locks

Slide 24

Slide 24 text

Types of locks 4 Unlocked

Slide 25

Slide 25 text

Types of locks 4 Unlocked 4 Shared

Slide 26

Slide 26 text

Types of locks 4 Unlocked 4 Shared 4 Reserved

Slide 27

Slide 27 text

Types of locks 4 Unlocked 4 Shared 4 Reserved 4 Pending

Slide 28

Slide 28 text

Types of locks 4 Unlocked 4 Shared 4 Reserved 4 Pending 4 Exclusive

Slide 29

Slide 29 text

Reading from the database

Slide 30

Slide 30 text

Writing to the database 1. Get share lock 2. Get reserved lock 3. Write to memory 4. Write to rollback journal

Slide 31

Slide 31 text

The rollback journal

Slide 32

Slide 32 text

Writing to the database 1. Obtain a PENDING lock 2. Obtain an EXCLUSIVE lock 3. Wait for SHARED locks to be done 4. Write to database

Slide 33

Slide 33 text

Write-Ahead Log (WAL)

Slide 34

Slide 34 text

WAL continued

Slide 35

Slide 35 text

Writes with WAL 1. Append to WAL 2. COMMIT 3. Continue writing 4. Checkpoint- write to file

Slide 36

Slide 36 text

Reads with WAL 1. Check WAL 2. Check DB

Slide 37

Slide 37 text

Database connections

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

SQLite connections

Slide 40

Slide 40 text

Android APIs

Slide 41

Slide 41 text

Only one write connection and multiple reads

Slide 42

Slide 42 text

Get a connection to SQLite

Slide 43

Slide 43 text

package android.database.sqlite; public abstract class SQLiteOpenHelper { public SQLiteDatabase getWritableDatabase() { synchronized (this) { return getDatabaseLocked(true); } } public SQLiteDatabase getReadableDatabase() { synchronized (this) { return getDatabaseLocked(false); } } }

Slide 44

Slide 44 text

public final class SQLiteDatabase extends SQLiteClosable { // The connection pool for the database, null when closed. // The pool itself is thread-safe, but the reference to it can only be acquired // when the lock is held. // INVARIANT: Guarded by mLock. private SQLiteConnectionPool mConnectionPoolLocked; }

Slide 45

Slide 45 text

Change SQLite settings public final class SQLiteDatabase extends SQLiteClosable { public boolean enableWriteAheadLogging() { ... mConfigurationLocked.openFlags |= ENABLE_WRITE_AHEAD_LOGGING; try { mConnectionPoolLocked.reconfigure(mConfigurationLocked); } catch (RuntimeException ex) { mConfigurationLocked.openFlags &= ~ENABLE_WRITE_AHEAD_LOGGING; throw ex; } } }

Slide 46

Slide 46 text

public final class SQLiteConnection ... { private void setWalModeFromConfiguration() { ... setJournalMode("WAL"); ... } } }

Slide 47

Slide 47 text

private void setJournalMode(String newValue) { String value = executeForString("PRAGMA journal_mode", null, null); if (!value.equalsIgnoreCase(newValue)) { try { String result = executeForString("PRAGMA journal_mode=" + newValue, null, null); if (result.equalsIgnoreCase(newValue)) { return; } ... }

Slide 48

Slide 48 text

Where Room sets up WAL public abstract class RoomDatabase { public void init(...) { boolean wal = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { wal = configuration.journalMode == JournalMode.WRITE_AHEAD_LOGGING; mOpenHelper.setWriteAheadLoggingEnabled(wal); } ... } }

Slide 49

Slide 49 text

Room with threads

Slide 50

Slide 50 text

Queries (reads) in Room

Slide 51

Slide 51 text

Writes in Room?

Slide 52

Slide 52 text

Transactions

Slide 53

Slide 53 text

Transactions in SQLite

Slide 54

Slide 54 text

SQLite BEGIN TRANSACTION

Slide 55

Slide 55 text

SQLite COMMIT

Slide 56

Slide 56 text

Transaction on journal vs WAL

Slide 57

Slide 57 text

Transactions / writes with Android API

Slide 58

Slide 58 text

Room wraps a lot of things in transactions

Slide 59

Slide 59 text

Multiple write statements - same transaction val preparedQuery = prepareQuery("INSERT...") beginTransaction input.forEach { bind(preparedQuery, it) } endTransaction

Slide 60

Slide 60 text

Triggers

Slide 61

Slide 61 text

Triggers in SQLite

Slide 62

Slide 62 text

Triggers for RxJava and LiveData class RoomTrackingLiveData extends LiveData { final Runnable mInvalidationRunnable = new Runnable() { ... public void run() { boolean isActive = hasActiveObservers(); if (mInvalid.compareAndSet(false, true)) { if (isActive) { mDatabase.getQueryExecutor().execute(mRefreshRunnable); } } } }; }

Slide 63

Slide 63 text

class RoomTrackingLiveData extends LiveData { final Runnable mRefreshRunnable = new Runnable() { public void run() { ... mDatabase.getInvalidationTracker().addWeakObserver(mObserver); }

Slide 64

Slide 64 text

Invalidation Tracking public class InvalidationTracker { static class ObservedTableTracker { static final int NO_OP = 0; // don't change trigger state for this table static final int ADD = 1; // add triggers for this table static final int REMOVE = 2; // remove triggers for this table

Slide 65

Slide 65 text

public class InvalidationTracker { void syncTriggers(SupportSQLiteDatabase database) { try { for (int tableId = 0; tableId < limit; tableId++) { switch (tablesToSync[tableId]) { case ObservedTableTracker.ADD: startTrackingTable(database, tableId); break; case ObservedTableTracker.REMOVE: stopTrackingTable(database, tableId); break; } } }

Slide 66

Slide 66 text

private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) { final String tableName = mTableNames[tableId]; StringBuilder stringBuilder = new StringBuilder(); for (String trigger : TRIGGERS) { stringBuilder.setLength(0); stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS "); appendTriggerName(stringBuilder, tableName, trigger); stringBuilder.append(" AFTER ") .append(trigger) .append(" ON `") .append(tableName) .append("` BEGIN INSERT OR REPLACE INTO ") .append(UPDATE_TABLE_NAME) .append(" VALUES(null, ") .append(tableId) .append("); END"); writableDb.execSQL(stringBuilder.toString()); } }

Slide 67

Slide 67 text

private Set checkUpdatedTable() { HashSet invalidatedTableIds = new HashSet<>(); Cursor cursor = mDatabase.query(new SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL)); //noinspection TryFinallyCanBeTryWithResources try { while (cursor.moveToNext()) { final int tableId = cursor.getInt(0); invalidatedTableIds.add(tableId); } } finally { cursor.close(); } if (!invalidatedTableIds.isEmpty()) { mCleanupStatement.executeUpdateDelete(); } return invalidatedTableIds; }

Slide 68

Slide 68 text

public void endTransaction() { mInvalidationTracker.refreshVersionsAsync(); }

Slide 69

Slide 69 text

AFTER INSERT/UPDATE/DELETE ON BEGIN UPDATE SET invalidated = 1 WHERE tableId = AND invalidated = 0; END

Slide 70

Slide 70 text

Transactions with Room

Slide 71

Slide 71 text

TransactionExecutor

Slide 72

Slide 72 text

class TransactionExecutor implements Executor { private final Executor mExecutor; private final ArrayDeque mTasks = new ArrayDeque<>(); private Runnable mActive; TransactionExecutor(@NonNull Executor executor) { mExecutor = executor; } public synchronized void execute(final Runnable command) { mTasks.offer(new Runnable() { public void run() { try { command.run(); } finally { scheduleNext(); } } }); if (mActive == null) { scheduleNext(); } } @SuppressWarnings("WeakerAccess") synchronized void scheduleNext() { if ((mActive = mTasks.poll()) != null) { mExecutor.execute(mActive); } } }

Slide 73

Slide 73 text

In RxJava public class RxRoom { @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static Observable createObservable(final RoomDatabase database, final boolean inTransaction, final String[] tableNames, final Callable callable) { Scheduler scheduler = Schedulers.from(getExecutor(database, inTransaction)); final Maybe maybe = Maybe.fromCallable(callable); return createObservable(database, tableNames) .subscribeOn(scheduler) .unsubscribeOn(scheduler) .observeOn(scheduler) .flatMapMaybe(new Function>() { @Override public MaybeSource apply(Object o) throws Exception { return maybe; } }); } private static Executor getExecutor(RoomDatabase database, boolean inTransaction) { if (inTransaction) { return database.getTransactionExecutor(); } else { return database.getQueryExecutor(); } } }

Slide 74

Slide 74 text

In Coroutines class CoroutinesRoom private constructor() { companion object { @JvmStatic suspend fun execute( db: RoomDatabase, inTransaction: Boolean, callable: Callable ): R { if (db.isOpen && db.inTransaction()) { return callable.call() } // Use the transaction dispatcher if we are on a transaction coroutine, otherwise // use the database dispatchers. val context = coroutineContext[TransactionElement]?.transactionDispatcher ?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher return withContext(context) { callable.call() } } } }

Slide 75

Slide 75 text

Transaction APIs in Room public abstract class RoomDatabase { @Deprecated public void beginTransaction() { assertNotMainThread(); SupportSQLiteDatabase database = mOpenHelper.getWritableDatabase(); mInvalidationTracker.syncTriggers(database); database.beginTransaction(); } /** * Wrapper for {@link SupportSQLiteDatabase#endTransaction()}. * * @deprecated Use {@link #runInTransaction(Runnable)} */ @Deprecated public void endTransaction() { mOpenHelper.getWritableDatabase().endTransaction(); if (!inTransaction()) { // enqueue refresh only if we are NOT in a transaction. Otherwise, wait for the last // endTransaction call to do it. mInvalidationTracker.refreshVersionsAsync(); } } }

Slide 76

Slide 76 text

public abstract class RoomDatabase { public void runInTransaction(@NonNull Runnable body) { beginTransaction(); try { body.run(); setTransactionSuccessful(); } finally { endTransaction(); } } }

Slide 77

Slide 77 text

What can developers Do?

Slide 78

Slide 78 text

Conclusions

Slide 79

Slide 79 text

Large queries and paging in Room

Slide 80

Slide 80 text

Cursor - Android API package android.database.sqlite; public class SQLiteCursor extends AbstractWindowedCursor { private void fillWindow(int requiredPos) { ... } }

Slide 81

Slide 81 text

So large queries are...

Slide 82

Slide 82 text

Why is this a problem? 1. SQLiteCursor isn't in a transaction

Slide 83

Slide 83 text

Why is this a problem? 1. SQLiteCursor isn't in a transaction 2. SQLiteCursor cursorPickFillWindowStartPosition

Slide 84

Slide 84 text

/** * Picks a start position for {@link Cursor#fillWindow} such that the * window will contain the requested row and a useful range of rows * around it. * * When the data set is too large to fit in a cursor window, seeking the * cursor can become a very expensive operation since we have to run the * query again when we move outside the bounds of the current window. * * We try to choose a start position for the cursor window such that * 1/3 of the window's capacity is used to hold rows before the requested * position and 2/3 of the window's capacity is used to hold rows after the * requested position. * * @param cursorPosition The row index of the row we want to get. * @param cursorWindowCapacity The estimated number of rows that can fit in * a cursor window, or 0 if unknown. * @return The recommended start position, always less than or equal to * the requested row. * @hide */ public static int cursorPickFillWindowStartPosition( int cursorPosition, int cursorWindowCapacity) { return Math.max(cursorPosition - cursorWindowCapacity / 3, 0); }

Slide 85

Slide 85 text

Solution #1: @Transaction https://medium.com/androiddevelopers/7-pro-tips- for-room-fbadea4bfbd1

Slide 86

Slide 86 text

Solution #2: Write smaller queries!

Slide 87

Slide 87 text

Solution #3: Integrate with paging library package androidx.room.paging; public abstract class LimitOffsetDataSource extends PositionalDataSource { public List loadRange(int startPosition, int loadCount) { final RoomSQLiteQuery sqLiteQuery = getSQLiteQuery(startPosition, loadCount); if (mInTransaction) { mDb.beginTransaction(); Cursor cursor = null; //noinspection TryFinallyCanBeTryWithResources try { cursor = mDb.query(sqLiteQuery); List rows = convertRows(cursor); mDb.setTransactionSuccessful(); return rows; } finally { if (cursor != null) { cursor.close(); } mDb.endTransaction(); sqLiteQuery.release(); } } else { Cursor cursor = mDb.query(sqLiteQuery); //noinspection TryFinallyCanBeTryWithResources try { return convertRows(cursor); } finally { cursor.close(); sqLiteQuery.release(); } } } }

Slide 88

Slide 88 text

mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?";

Slide 89

Slide 89 text

Paging + LiveData + Room support LiveData> users = new LivePagedListBuilder<>( userDao.loadUsersByAgeDesc(), /*page size*/ 20).build(); https://github.com/googlesamples/android- architecture-components/tree/master/PagingSample

Slide 90

Slide 90 text

Thanks! Effie Barak (@CodingChick)