/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.room; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.Context; import android.database.Cursor; import android.os.Build; import android.util.Log; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.WorkerThread; import androidx.collection.SparseArrayCompat; import androidx.core.app.ActivityManagerCompat; import androidx.arch.core.executor.ArchTaskExecutor; import androidx.room.migration.Migration; import androidx.sqlite.db.SimpleSQLiteQuery; import androidx.sqlite.db.SupportSQLiteDatabase; import androidx.sqlite.db.SupportSQLiteOpenHelper; import androidx.sqlite.db.SupportSQLiteQuery; import androidx.sqlite.db.SupportSQLiteStatement; import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Base class for all Room databases. All classes that are annotated with {@link Database} must * extend this class. *
* RoomDatabase provides direct access to the underlying database implementation but you should
* prefer using {@link Dao} classes.
*
* @see Database
*/
//@SuppressWarnings({"unused", "WeakerAccess"})
public abstract class RoomDatabase {
private static final String DB_IMPL_SUFFIX = "_Impl";
/**
* Unfortunately, we cannot read this value so we are only setting it to the SQLite default.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final int MAX_BIND_PARAMETER_CNT = 999;
// set by the generated open helper.
protected volatile SupportSQLiteDatabase mDatabase;
private SupportSQLiteOpenHelper mOpenHelper;
private final InvalidationTracker mInvalidationTracker;
private boolean mAllowMainThreadQueries;
boolean mWriteAheadLoggingEnabled;
@Nullable
protected List
* You cannot create an instance of a database, instead, you should acquire it via
* {@link Room#databaseBuilder(Context, Class, String)} or
* {@link Room#inMemoryDatabaseBuilder(Context, Class)}.
*/
public RoomDatabase() {
mInvalidationTracker = createInvalidationTracker();
}
/**
* Called by {@link Room} when it is initialized.
*
* @param configuration The database configuration.
*/
@CallSuper
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);
}
mCallbacks = configuration.callbacks;
mAllowMainThreadQueries = configuration.allowMainThreadQueries;
mWriteAheadLoggingEnabled = wal;
}
/**
* Returns the SQLite open helper used by this database.
*
* @return The SQLite open helper used by this database.
*/
@NonNull
public SupportSQLiteOpenHelper getOpenHelper() {
return mOpenHelper;
}
/**
* Creates the open helper to access the database. Generated class already implements this
* method.
* Note that this method is called when the RoomDatabase is initialized.
*
* @param config The configuration of the Room database.
* @return A new SupportSQLiteOpenHelper to be used while connecting to the database.
*/
@NonNull
protected abstract SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config);
/**
* Called when the RoomDatabase is created.
*
* This is already implemented by the generated code.
*
* @return Creates a new InvalidationTracker.
*/
@NonNull
protected abstract InvalidationTracker createInvalidationTracker();
/**
* Deletes all rows from all the tables that are registered to this database as
* {@link Database#entities()}.
*
* This does NOT reset the auto-increment value generated by {@link PrimaryKey#autoGenerate()}.
*
* After deleting the rows, Room will set a WAL checkpoint and run VACUUM. This means that the
* data is completely erased. The space will be reclaimed by the system if the amount surpasses
* the threshold of database file size.
*
* @see Database File Format
*/
@WorkerThread
public abstract void clearAllTables();
/**
* Returns true if database connection is open and initialized.
*
* @return true if the database connection is open, false otherwise.
*/
public boolean isOpen() {
final SupportSQLiteDatabase db = mDatabase;
return db != null && db.isOpen();
}
/**
* Closes the database if it is already open.
*/
public void close() {
if (isOpen()) {
try {
mCloseLock.lock();
mOpenHelper.close();
} finally {
mCloseLock.unlock();
}
}
}
/**
* Asserts that we are not on the main thread.
*
* @hide
*/
@SuppressWarnings("WeakerAccess")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
// used in generated code
public void assertNotMainThread() {
if (mAllowMainThreadQueries) {
return;
}
if (ArchTaskExecutor.getInstance().isMainThread()) {
throw new IllegalStateException("Cannot access database on the main thread since"
+ " it may potentially lock the UI for a long period of time.");
}
}
// Below, there are wrapper methods for SupportSQLiteDatabase. This helps us track which
// methods we are using and also helps unit tests to mock this class without mocking
// all SQLite database methods.
/**
* Convenience method to query the database with arguments.
*
* @param query The sql query
* @param args The bind arguments for the placeholders in the query
*
* @return A Cursor obtained by running the given query in the Room database.
*/
public Cursor query(String query, @Nullable Object[] args) {
return mOpenHelper.getWritableDatabase().query(new SimpleSQLiteQuery(query, args));
}
/**
* Wrapper for {@link SupportSQLiteDatabase#query(SupportSQLiteQuery)}.
*
* @param query The Query which includes the SQL and a bind callback for bind arguments.
* @return Result of the query.
*/
public Cursor query(SupportSQLiteQuery query) {
assertNotMainThread();
return mOpenHelper.getWritableDatabase().query(query);
}
/**
* Wrapper for {@link SupportSQLiteDatabase#compileStatement(String)}.
*
* @param sql The query to compile.
* @return The compiled query.
*/
public SupportSQLiteStatement compileStatement(@NonNull String sql) {
assertNotMainThread();
return mOpenHelper.getWritableDatabase().compileStatement(sql);
}
/**
* Wrapper for {@link SupportSQLiteDatabase#beginTransaction()}.
*/
public void beginTransaction() {
assertNotMainThread();
SupportSQLiteDatabase database = mOpenHelper.getWritableDatabase();
mInvalidationTracker.syncTriggers(database);
database.beginTransaction();
}
/**
* Wrapper for {@link SupportSQLiteDatabase#endTransaction()}.
*/
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();
}
}
/**
* Wrapper for {@link SupportSQLiteDatabase#setTransactionSuccessful()}.
*/
public void setTransactionSuccessful() {
mOpenHelper.getWritableDatabase().setTransactionSuccessful();
}
/**
* Executes the specified {@link Runnable} in a database transaction. The transaction will be
* marked as successful unless an exception is thrown in the {@link Runnable}.
*
* @param body The piece of code to execute.
*/
public void runInTransaction(@NonNull Runnable body) {
beginTransaction();
try {
body.run();
setTransactionSuccessful();
} finally {
endTransaction();
}
}
/**
* Executes the specified {@link Callable} in a database transaction. The transaction will be
* marked as successful unless an exception is thrown in the {@link Callable}.
*
* @param body The piece of code to execute.
* @param
* You should never call this method manually.
*
* @param db The database instance.
*/
protected void internalInitInvalidationTracker(@NonNull SupportSQLiteDatabase db) {
mInvalidationTracker.internalInit(db);
}
/**
* Returns the invalidation tracker for this database.
*
* You can use the invalidation tracker to get notified when certain tables in the database
* are modified.
*
* @return The invalidation tracker for the database.
*/
@NonNull
public InvalidationTracker getInvalidationTracker() {
return mInvalidationTracker;
}
/**
* Returns true if current thread is in a transaction.
*
* @return True if there is an active transaction in current thread, false otherwise.
* @see SupportSQLiteDatabase#inTransaction()
*/
@SuppressWarnings("WeakerAccess")
public boolean inTransaction() {
return mOpenHelper.getWritableDatabase().inTransaction();
}
/**
* Journal modes for SQLite database.
*
* @see RoomDatabase.Builder#setJournalMode(JournalMode)
*/
public enum JournalMode {
/**
* Let Room choose the journal mode. This is the default value when no explicit value is
* specified.
*
* The actual value will be {@link #TRUNCATE} when the device runs API Level lower than 16
* or it is a low-RAM device. Otherwise, {@link #WRITE_AHEAD_LOGGING} will be used.
*/
AUTOMATIC,
/**
* Truncate journal mode.
*/
TRUNCATE,
/**
* Write-Ahead Logging mode.
*/
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
WRITE_AHEAD_LOGGING;
/**
* Resolves {@link #AUTOMATIC} to either {@link #TRUNCATE} or
* {@link #WRITE_AHEAD_LOGGING}.
*/
@SuppressLint("NewApi")
JournalMode resolve(Context context) {
if (this != AUTOMATIC) {
return this;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
ActivityManager manager = (ActivityManager)
context.getSystemService(Context.ACTIVITY_SERVICE);
if (manager != null && !ActivityManagerCompat.isLowRamDevice(manager)) {
return WRITE_AHEAD_LOGGING;
}
}
return TRUNCATE;
}
}
/**
* Builder for RoomDatabase.
*
* @param
* Each Migration has a start and end versions and Room runs these migrations to bring the
* database to the latest version.
*
* If a migration item is missing between current version and the latest version, Room
* will clear the database and recreate so even if you have no changes between 2 versions,
* you should still provide a Migration object to the builder.
*
* A migration can handle more than 1 version (e.g. if you have a faster path to choose when
* going version 3 to 5 without going to version 4). If Room opens a database at version
* 3 and latest version is >= 5, Room will use the migration object that can migrate from
* 3 to 5 instead of 3 to 4 and 4 to 5.
*
* @param migrations The migration object that can modify the database and to the necessary
* changes.
* @return this
*/
@NonNull
public Builder
* Room ensures that Database is never accessed on the main thread because it may lock the
* main thread and trigger an ANR. If you need to access the database from the main thread,
* you should always use async alternatives or manually move the call to a background
* thread.
*
* You may want to turn this check off for testing.
*
* @return this
*/
@NonNull
public Builder
* This value is ignored if the builder is initialized with
* {@link Room#inMemoryDatabaseBuilder(Context, Class)}.
*
* The journal mode should be consistent across multiple instances of
* {@link RoomDatabase} for a single SQLite database file.
*
* The default value is {@link JournalMode#AUTOMATIC}.
*
* @param journalMode The journal mode.
* @return this
*/
@NonNull
public Builder
* When the database version on the device does not match the latest schema version, Room
* runs necessary {@link Migration}s on the database.
*
* If it cannot find the set of {@link Migration}s that will bring the database to the
* current version, it will throw an {@link IllegalStateException}.
*
* You can call this method to change this behavior to re-create the database instead of
* crashing.
*
* Note that this will delete all of the data in the database tables managed by Room.
*
* @return this
*/
@NonNull
public Builder
* This functionality is the same as that provided by
* {@link #fallbackToDestructiveMigration()}, except that this method allows the
* specification of a set of schema versions for which destructive recreation is allowed.
*
* Using this method is preferable to {@link #fallbackToDestructiveMigration()} if you want
* to allow destructive migrations from some schema versions while still taking advantage
* of exceptions being thrown due to unintentionally missing migrations.
*
* Note: No versions passed to this method may also exist as either starting or ending
* versions in the {@link Migration}s provided to {@link #addMigrations(Migration...)}. If a
* version passed to this method is found as a starting or ending version in a Migration, an
* exception will be thrown.
*
* @param startVersions The set of schema versions from which Room should use a destructive
* migration.
* @return this
*/
@NonNull
public Builder
* By default, all RoomDatabases use in memory storage for TEMP tables and enables recursive
* triggers.
*
* @return A new database instance.
*/
@NonNull
public T build() {
//noinspection ConstantConditions
if (mContext == null) {
throw new IllegalArgumentException("Cannot provide null context for the database.");
}
//noinspection ConstantConditions
if (mDatabaseClass == null) {
throw new IllegalArgumentException("Must provide an abstract class that"
+ " extends RoomDatabase");
}
if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) {
for (Integer version : mMigrationStartAndEndVersions) {
if (mMigrationsNotRequiredFrom.contains(version)) {
throw new IllegalArgumentException(
"Inconsistency detected. A Migration was supplied to "
+ "addMigration(Migration... migrations) that has a start "
+ "or end version equal to a start version supplied to "
+ "fallbackToDestructiveMigrationFrom(int... "
+ "startVersions). Start version: "
+ version);
}
}
}
if (mFactory == null) {
mFactory = new FrameworkSQLiteOpenHelperFactory();
}
DatabaseConfiguration configuration =
new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
mCallbacks, mAllowMainThreadQueries,
mJournalMode.resolve(mContext),
mRequireMigration, mMigrationsNotRequiredFrom);
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
db.init(configuration);
return db;
}
}
/**
* A container to hold migrations. It also allows querying its contents to find migrations
* between two versions.
*/
public static class MigrationContainer {
private SparseArrayCompat