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

Intro to Android and Android x APIs

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Android X androidx.sqlite.db SupportSQLiteDatabase SupportSQLiteOpenHelper SupportSQLiteQuery

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Intro to Room Architecture

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Threads, processes etc

Slide 14

Slide 14 text

Locking in SQLite

Slide 15

Slide 15 text

Types of locks

Slide 16

Slide 16 text

Types of locks 4 Unlocked

Slide 17

Slide 17 text

Types of locks 4 Unlocked 4 Shared

Slide 18

Slide 18 text

Types of locks 4 Unlocked 4 Shared 4 Reserved

Slide 19

Slide 19 text

Types of locks 4 Unlocked 4 Shared 4 Reserved 4 Pending

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Reading from the database

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

The rollback journal

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Write-Ahead Log (WAL)

Slide 26

Slide 26 text

WAL continued

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Reads with WAL 1. Check WAL 2. Check DB

Slide 29

Slide 29 text

Database connections

Slide 30

Slide 30 text

SQLite connections

Slide 31

Slide 31 text

Android APIs

Slide 32

Slide 32 text

Only one write connection and multiple reads

Slide 33

Slide 33 text

Get a connection to SQLite

Slide 34

Slide 34 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 35

Slide 35 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 36

Slide 36 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 37

Slide 37 text

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

Slide 38

Slide 38 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; } // PRAGMA journal_mode silently fails and returns the original journal // mode in some cases if the journal mode could not be changed. } catch (SQLiteDatabaseLockedException ex) { // This error (SQLITE_BUSY) occurs if one connection has the database // open in WAL mode and another tries to change it to non-WAL. } ... }

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Room with threads

Slide 41

Slide 41 text

Queries (reads) in Room

Slide 42

Slide 42 text

Writes in Room?

Slide 43

Slide 43 text

Transactions

Slide 44

Slide 44 text

Transactions in SQLite

Slide 45

Slide 45 text

SQLite BEGIN TRANSACTION

Slide 46

Slide 46 text

SQLite COMMIT

Slide 47

Slide 47 text

Transaction on journal vs WAL

Slide 48

Slide 48 text

Transactions / writes with Android API

Slide 49

Slide 49 text

Room wraps a lot of things in transactions

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Triggers

Slide 52

Slide 52 text

Triggers in SQLite

Slide 53

Slide 53 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 54

Slide 54 text

final Runnable mRefreshRunnable = new Runnable() { @WorkerThread @Override public void run() { if (mRegisteredObserver.compareAndSet(false, true)) { mDatabase.getInvalidationTracker().addWeakObserver(mObserver); }

Slide 55

Slide 55 text

Invalidation Tracking public class InvalidationTracker { @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public void addWeakObserver(Observer observer) { addObserver(new WeakObserver(this, observer)); } }

Slide 56

Slide 56 text

public class InvalidationTracker { void syncTriggers(SupportSQLiteDatabase database) { } 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 57

Slide 57 text

private boolean checkUpdatedTable() { boolean hasUpdatedTable = false; Cursor cursor = mDatabase.query(new SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL)); //noinspection TryFinallyCanBeTryWithResources try { while (cursor.moveToNext()) { final int tableId = cursor.getInt(0); mTableInvalidStatus.set(tableId); hasUpdatedTable = true; } } finally { cursor.close(); } if (hasUpdatedTable) { mCleanupStatement.executeUpdateDelete(); } return hasUpdatedTable; }

Slide 58

Slide 58 text

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 59

Slide 59 text

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

Slide 60

Slide 60 text

Transactions with Room

Slide 61

Slide 61 text

TransactionExecutor

Slide 62

Slide 62 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 63

Slide 63 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 64

Slide 64 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 65

Slide 65 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 66

Slide 66 text

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

Slide 67

Slide 67 text

What can developers Do?

Slide 68

Slide 68 text

Conclusions

Slide 69

Slide 69 text

Large queries and paging in Room

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

So large queries are...

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 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 75

Slide 75 text

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

Slide 76

Slide 76 text

Solution #2: Write smaller queries!

Slide 77

Slide 77 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 78

Slide 78 text

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

Slide 79

Slide 79 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 80

Slide 80 text

Thanks! Effie Barak (@CodingChick)