FairEmail/app/src/main/java/androidx/room/InvalidationTracker.kt

840 lines
31 KiB
Kotlin

/*
* Copyright (C) 2017 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.content.Context
import android.content.Intent
import android.database.sqlite.SQLiteException
import android.os.Build
import android.util.Log
import androidx.annotation.GuardedBy
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.arch.core.internal.SafeIterableMap
import androidx.lifecycle.LiveData
import androidx.room.Room.LOG_TAG
import androidx.room.util.useCursor
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteStatement
import java.lang.ref.WeakReference
import java.util.Arrays
import java.util.Locale
import java.util.concurrent.Callable
import java.util.concurrent.atomic.AtomicBoolean
/**
* InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
* these tables.
*/
// Some details on how the InvalidationTracker works:
// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
// tables.
// * Each update (write operation) on one of the observed tables triggers an update into the
// memory table table, flipping the invalidated flag ON.
// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
// It works as an Observer, and notifies other instances of table invalidation.
open class InvalidationTracker @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) constructor(
internal val database: RoomDatabase,
private val shadowTablesMap: Map<String, String>,
private val viewTables: Map<String, @JvmSuppressWildcards Set<String>>,
vararg tableNames: String
) {
internal val tableIdLookup: Map<String, Int>
internal val tablesNames: Array<out String>
private var autoCloser: AutoCloser? = null
@get:RestrictTo(RestrictTo.Scope.LIBRARY)
@field:RestrictTo(RestrictTo.Scope.LIBRARY)
val pendingRefresh = AtomicBoolean(false)
@Volatile
private var initialized = false
@Volatile
internal var cleanupStatement: SupportSQLiteStatement? = null
private val observedTableTracker: ObservedTableTracker = ObservedTableTracker(tableNames.size)
private val invalidationLiveDataContainer: InvalidationLiveDataContainer =
InvalidationLiveDataContainer(database)
@GuardedBy("observerMap")
internal val observerMap = SafeIterableMap<Observer, ObserverWrapper>()
private var multiInstanceInvalidationClient: MultiInstanceInvalidationClient? = null
private val syncTriggersLock = Any()
private val trackerLock = Any()
init {
tableIdLookup = mutableMapOf()
tablesNames = Array(tableNames.size) { id ->
val tableName = tableNames[id].lowercase(Locale.US)
tableIdLookup[tableName] = id
val shadowTableName = shadowTablesMap[tableNames[id]]?.lowercase(Locale.US)
shadowTableName ?: tableName
}
// Adjust table id lookup for those tables whose shadow table is another already mapped
// table (e.g. external content fts tables).
shadowTablesMap.forEach { entry ->
val shadowTableName = entry.value.lowercase(Locale.US)
if (tableIdLookup.containsKey(shadowTableName)) {
val tableName = entry.key.lowercase(Locale.US)
tableIdLookup[tableName] = tableIdLookup.getValue(shadowTableName)
}
}
}
/**
* Used by the generated code.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
constructor(database: RoomDatabase, vararg tableNames: String) :
this(
database = database,
shadowTablesMap = emptyMap(),
viewTables = emptyMap(),
tableNames = tableNames
)
/**
* Sets the auto closer for this invalidation tracker so that the invalidation tracker can
* ensure that the database is not closed if there are pending invalidations that haven't yet
* been flushed.
*
* This also adds a callback to the autocloser to ensure that the InvalidationTracker is in
* an ok state once the table is invalidated.
*
* This must be called before the database is used.
*
* @param autoCloser the autocloser associated with the db
*/
internal fun setAutoCloser(autoCloser: AutoCloser) {
this.autoCloser = autoCloser
autoCloser.setAutoCloseCallback(::onAutoCloseCallback)
}
/**
* Internal method to initialize table tracking.
*
* You should never call this method, it is called by the generated code.
*/
internal fun internalInit(database: SupportSQLiteDatabase) {
synchronized(trackerLock) {
if (initialized) {
Log.e(LOG_TAG, "Invalidation tracker is initialized twice :/.")
return
}
// These actions are not in a transaction because temp_store is not allowed to be
// performed on a transaction, and recursive_triggers is not affected by transactions.
database.execSQL("PRAGMA temp_store = MEMORY;")
database.execSQL("PRAGMA recursive_triggers='ON';")
database.execSQL(CREATE_TRACKING_TABLE_SQL)
syncTriggers(database)
cleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL)
initialized = true
}
}
private fun onAutoCloseCallback() {
synchronized(trackerLock) {
initialized = false
observedTableTracker.resetTriggerState()
cleanupStatement?.close()
}
}
internal fun startMultiInstanceInvalidation(
context: Context,
name: String,
serviceIntent: Intent
) {
multiInstanceInvalidationClient = MultiInstanceInvalidationClient(
context = context,
name = name,
serviceIntent = serviceIntent,
invalidationTracker = this,
executor = database.queryExecutor
)
}
internal fun stopMultiInstanceInvalidation() {
multiInstanceInvalidationClient?.stop()
multiInstanceInvalidationClient = null
}
private fun stopTrackingTable(db: SupportSQLiteDatabase, tableId: Int) {
val tableName = tablesNames[tableId]
for (trigger in TRIGGERS) {
val sql = buildString {
append("DROP TRIGGER IF EXISTS ")
append(getTriggerName(tableName, trigger))
}
db.execSQL(sql)
}
}
private fun startTrackingTable(db: SupportSQLiteDatabase, tableId: Int) {
db.execSQL(
"INSERT OR IGNORE INTO $UPDATE_TABLE_NAME VALUES($tableId, 0)"
)
val tableName = tablesNames[tableId]
for (trigger in TRIGGERS) {
val sql = buildString {
append("CREATE TEMP TRIGGER IF NOT EXISTS ")
append(getTriggerName(tableName, trigger))
append(" AFTER ")
append(trigger)
append(" ON `")
append(tableName)
append("` BEGIN UPDATE ")
append(UPDATE_TABLE_NAME)
append(" SET ").append(INVALIDATED_COLUMN_NAME)
append(" = 1")
append(" WHERE ").append(TABLE_ID_COLUMN_NAME)
append(" = ").append(tableId)
append(" AND ").append(INVALIDATED_COLUMN_NAME)
append(" = 0")
append("; END")
}
db.execSQL(sql)
}
}
/**
* Adds the given observer to the observers list and it will be notified if any table it
* observes changes.
*
* Database changes are pulled on another thread so in some race conditions, the observer might
* be invoked for changes that were done before it is added.
*
* If the observer already exists, this is a no-op call.
*
* If one of the tables in the Observer does not exist in the database, this method throws an
* [IllegalArgumentException].
*
* This method should be called on a background/worker thread as it performs database
* operations.
*
* @param observer The observer which listens the database for changes.
*/
@SuppressLint("RestrictedApi")
@WorkerThread
open fun addObserver(observer: Observer) {
val tableNames = resolveViews(observer.tables)
val tableIds = tableNames.map { tableName ->
tableIdLookup[tableName.lowercase(Locale.US)]
?: throw IllegalArgumentException("There is no table with name $tableName")
}.toIntArray()
val wrapper = ObserverWrapper(
observer = observer,
tableIds = tableIds,
tableNames = tableNames
)
val currentObserver = synchronized(observerMap) {
observerMap.putIfAbsent(observer, wrapper)
}
if (currentObserver == null && observedTableTracker.onAdded(*tableIds)) {
syncTriggers()
}
}
private fun validateAndResolveTableNames(tableNames: Array<out String>): Array<out String> {
val resolved = resolveViews(tableNames)
resolved.forEach { tableName ->
require(tableIdLookup.containsKey(tableName.lowercase(Locale.US))) {
"There is no table with name $tableName"
}
}
return resolved
}
/**
* Resolves the list of tables and views into a list of unique tables that are underlying them.
*
* @param names The names of tables or views.
* @return The names of the underlying tables.
*/
private fun resolveViews(names: Array<out String>): Array<out String> {
return buildSet {
names.forEach { name ->
if (viewTables.containsKey(name.lowercase(Locale.US))) {
addAll(viewTables[name.lowercase(Locale.US)]!!)
} else {
add(name)
}
}
}.toTypedArray()
}
/**
* Adds an observer but keeps a weak reference back to it.
*
* Note that you cannot remove this observer once added. It will be automatically removed
* when the observer is GC'ed.
*
* @param observer The observer to which InvalidationTracker will keep a weak reference.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
open fun addWeakObserver(observer: Observer) {
addObserver(WeakObserver(this, observer))
}
/**
* Removes the observer from the observers list.
*
* This method should be called on a background/worker thread as it performs database
* operations.
*
* @param observer The observer to remove.
*/
@SuppressLint("RestrictedApi")
@WorkerThread
open fun removeObserver(observer: Observer) {
val wrapper = synchronized(observerMap) {
observerMap.remove(observer)
}
if (wrapper != null && observedTableTracker.onRemoved(tableIds = wrapper.tableIds)) {
syncTriggers()
}
}
internal fun ensureInitialization(): Boolean {
if (!database.isOpenInternal) {
return false
}
if (!initialized) {
// trigger initialization
database.openHelper.writableDatabase
}
if (!initialized) {
Log.e(LOG_TAG, "database is not initialized even though it is open")
return false
}
return true
}
@VisibleForTesting
@JvmField
@RestrictTo(RestrictTo.Scope.LIBRARY)
val refreshRunnable: Runnable = object : Runnable {
override fun run() {
val closeLock = database.getCloseLock()
closeLock.lock()
val invalidatedTableIds: Set<Int> =
try {
if (!ensureInitialization()) {
return
}
if (!pendingRefresh.compareAndSet(true, false)) {
// no pending refresh
return
}
if (database.inTransaction()) {
// current thread is in a transaction. when it ends, it will invoke
// refreshRunnable again. pendingRefresh is left as false on purpose
// so that the last transaction can flip it on again.
return
}
// This transaction has to be on the underlying DB rather than the RoomDatabase
// in order to avoid a recursive loop after endTransaction.
val db = database.openHelper.writableDatabase
db.beginTransactionNonExclusive()
val invalidatedTableIds: Set<Int>
try {
invalidatedTableIds = checkUpdatedTable()
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
invalidatedTableIds
} catch (ex: IllegalStateException) {
// may happen if db is closed. just log.
Log.e(
LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
ex
)
emptySet()
} catch (ex: SQLiteException) {
Log.e(
LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
ex
)
emptySet()
} finally {
closeLock.unlock()
autoCloser?.decrementCountAndScheduleClose()
}
if (invalidatedTableIds.isNotEmpty()) {
synchronized(observerMap) {
observerMap.forEach {
it.value.notifyByTableInvalidStatus(invalidatedTableIds)
}
}
}
}
private fun checkUpdatedTable(): Set<Int> {
val invalidatedTableIds = buildSet {
database.query(SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL)).useCursor { cursor ->
while (cursor.moveToNext()) {
add(cursor.getInt(0))
}
}
}
if (invalidatedTableIds.isNotEmpty()) {
checkNotNull(cleanupStatement)
val statement = cleanupStatement
requireNotNull(statement)
statement.executeUpdateDelete()
}
return invalidatedTableIds
}
}
/**
* Enqueues a task to refresh the list of updated tables.
*
* This method is automatically called when [RoomDatabase.endTransaction] is called but
* if you have another connection to the database or directly use [ ], you may need to call this
* manually.
*/
open fun refreshVersionsAsync() {
// TODO we should consider doing this sync instead of async.
if (pendingRefresh.compareAndSet(false, true)) {
// refreshVersionsAsync is called with the ref count incremented from
// RoomDatabase, so the db can't be closed here, but we need to be sure that our
// db isn't closed until refresh is completed. This increment call must be
// matched with a corresponding call in refreshRunnable.
autoCloser?.incrementCountAndEnsureDbIsOpen()
database.queryExecutor.execute(refreshRunnable)
}
}
/**
* Check versions for tables, and run observers synchronously if tables have been updated.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@WorkerThread
open fun refreshVersionsSync() {
// This increment call must be matched with a corresponding call in refreshRunnable.
autoCloser?.incrementCountAndEnsureDbIsOpen()
syncTriggers()
refreshRunnable.run()
}
/**
* Notifies all the registered [Observer]s of table changes.
*
* This can be used for notifying invalidation that cannot be detected by this
* [InvalidationTracker], for example, invalidation from another process.
*
* @param tables The invalidated tables.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun notifyObserversByTableNames(vararg tables: String) {
synchronized(observerMap) {
observerMap.forEach { (observer, wrapper) ->
if (!observer.isRemote) {
wrapper.notifyByTableNames(tables)
}
}
}
}
internal fun syncTriggers(database: SupportSQLiteDatabase) {
if (database.inTransaction()) {
// we won't run this inside another transaction.
return
}
try {
val closeLock = this.database.getCloseLock()
closeLock.lock()
try {
// Serialize adding and removing table trackers, this is specifically important
// to avoid missing invalidation before a transaction starts but there are
// pending (possibly concurrent) observer changes.
synchronized(syncTriggersLock) {
val tablesToSync = observedTableTracker.getTablesToSync() ?: return
beginTransactionInternal(database)
try {
tablesToSync.forEachIndexed { tableId, syncState ->
when (syncState) {
ObservedTableTracker.ADD ->
startTrackingTable(database, tableId)
ObservedTableTracker.REMOVE ->
stopTrackingTable(database, tableId)
}
}
database.setTransactionSuccessful()
} finally {
database.endTransaction()
}
}
} finally {
closeLock.unlock()
}
} catch (ex: IllegalStateException) {
// may happen if db is closed. just log.
Log.e(LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", ex)
} catch (ex: SQLiteException) {
Log.e(LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", ex)
}
}
/**
* Called by RoomDatabase before each beginTransaction call.
*
* It is important that pending trigger changes are applied to the database before any query
* runs. Otherwise, we may miss some changes.
*
* This api should eventually be public.
*/
internal fun syncTriggers() {
if (!database.isOpenInternal) {
return
}
syncTriggers(database.openHelper.writableDatabase)
}
/**
* Creates a LiveData that computes the given function once and for every other invalidation
* of the database.
*
* Holds a strong reference to the created LiveData as long as it is active.
*
* @param computeFunction The function that calculates the value
* @param tableNames The list of tables to observe
* @param T The return type
* @return A new LiveData that computes the given function when the given list of tables
* invalidates.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Deprecated("Use [createLiveData(String[], boolean, Callable)]")
open fun <T> createLiveData(
tableNames: Array<out String>,
computeFunction: Callable<T?>
): LiveData<T> {
return createLiveData(tableNames, false, computeFunction)
}
/**
* Creates a LiveData that computes the given function once and for every other invalidation
* of the database.
*
* Holds a strong reference to the created LiveData as long as it is active.
*
* @param tableNames The list of tables to observe
* @param inTransaction True if the computeFunction will be done in a transaction, false
* otherwise.
* @param computeFunction The function that calculates the value
* @param T The return type
* @return A new LiveData that computes the given function when the given list of tables
* invalidates.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
open fun <T> createLiveData(
tableNames: Array<out String>,
inTransaction: Boolean,
computeFunction: Callable<T?>
): LiveData<T> {
return invalidationLiveDataContainer.create(
validateAndResolveTableNames(tableNames), inTransaction, computeFunction
)
}
/**
* Wraps an observer and keeps the table information.
*
* Internally table ids are used which may change from database to database so the table
* related information is kept here rather than in the Observer.
*/
internal class ObserverWrapper(
internal val observer: Observer,
internal val tableIds: IntArray,
private val tableNames: Array<out String>
) {
private val singleTableSet = if (tableNames.isNotEmpty()) {
setOf(tableNames[0])
} else {
emptySet()
}
init {
check(tableIds.size == tableNames.size)
}
/**
* Notifies the underlying [.mObserver] if any of the observed tables are invalidated
* based on the given invalid status set.
*
* @param invalidatedTablesIds The table ids of the tables that are invalidated.
*/
internal fun notifyByTableInvalidStatus(invalidatedTablesIds: Set<Int?>) {
val invalidatedTables = when (tableIds.size) {
0 -> emptySet()
1 -> if (invalidatedTablesIds.contains(tableIds[0])) {
singleTableSet // Optimization for a single-table observer
} else {
emptySet()
}
else -> buildSet {
tableIds.forEachIndexed { idx, tableId ->
if (invalidatedTablesIds.contains(tableId)) {
add(tableNames[idx])
}
}
}
}
if (invalidatedTables.isNotEmpty()) {
observer.onInvalidated(invalidatedTables)
}
}
/**
* Notifies the underlying [.mObserver] if it observes any of the specified
* `tables`.
*
* @param tables The invalidated table names.
*/
internal fun notifyByTableNames(tables: Array<out String>) {
val invalidatedTables = when (tableNames.size) {
0 -> emptySet()
1 -> if (tables.any { it.equals(tableNames[0], ignoreCase = true) }) {
singleTableSet // Optimization for a single-table observer
} else {
emptySet()
}
else -> buildSet {
tables.forEach { table ->
tableNames.forEach ourTablesLoop@{ ourTable ->
if (ourTable.equals(table, ignoreCase = true)) {
add(ourTable)
return@ourTablesLoop
}
}
}
}
}
if (invalidatedTables.isNotEmpty()) {
observer.onInvalidated(invalidatedTables)
}
}
}
/**
* An observer that can listen for changes in the database.
*/
abstract class Observer(internal val tables: Array<out String>) {
/**
* Observes the given list of tables and views.
*
* @param firstTable The name of the table or view.
* @param rest More names of tables or views.
*/
protected constructor(firstTable: String, vararg rest: String) : this(
buildList {
addAll(rest)
add(firstTable)
}.toTypedArray()
)
/**
* Called when one of the observed tables is invalidated in the database.
*
* @param tables A set of invalidated tables. This is useful when the observer targets
* multiple tables and you want to know which table is invalidated. This will
* be names of underlying tables when you are observing views.
*/
abstract fun onInvalidated(tables: Set<String>)
internal open val isRemote: Boolean
get() = false
}
/**
* Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
* triggers in the database.
*
* This class is thread safe
*/
internal class ObservedTableTracker(tableCount: Int) {
// number of observers per table
val tableObservers = LongArray(tableCount)
// trigger state for each table at last sync
// this field is updated when syncAndGet is called.
private val triggerStates = BooleanArray(tableCount)
// when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
private val triggerStateChanges = IntArray(tableCount)
var needsSync = false
/**
* @return true if # of triggers is affected.
*/
fun onAdded(vararg tableIds: Int): Boolean {
var needTriggerSync = false
synchronized(this) {
tableIds.forEach { tableId ->
val prevObserverCount = tableObservers[tableId]
tableObservers[tableId] = prevObserverCount + 1
if (prevObserverCount == 0L) {
needsSync = true
needTriggerSync = true
}
}
}
return needTriggerSync
}
/**
* @return true if # of triggers is affected.
*/
fun onRemoved(vararg tableIds: Int): Boolean {
var needTriggerSync = false
synchronized(this) {
tableIds.forEach { tableId ->
val prevObserverCount = tableObservers[tableId]
tableObservers[tableId] = prevObserverCount - 1
if (prevObserverCount == 1L) {
needsSync = true
needTriggerSync = true
}
}
}
return needTriggerSync
}
/**
* If we are re-opening the db we'll need to add all the triggers that we need so change
* the current state to false for all.
*/
fun resetTriggerState() {
synchronized(this) {
Arrays.fill(triggerStates, false)
needsSync = true
}
}
/**
* If this returns non-null, you must call onSyncCompleted.
*
* @return int[] An int array where the index for each tableId has the action for that
* table.
*/
@VisibleForTesting
@JvmName("getTablesToSync")
fun getTablesToSync(): IntArray? {
synchronized(this) {
if (!needsSync) {
return null
}
tableObservers.forEachIndexed { i, observerCount ->
val newState = observerCount > 0
if (newState != triggerStates[i]) {
triggerStateChanges[i] = if (newState) ADD else REMOVE
} else {
triggerStateChanges[i] = NO_OP
}
triggerStates[i] = newState
}
needsSync = false
return triggerStateChanges.clone()
}
}
internal companion object {
const val NO_OP = 0 // don't change trigger state for this table
const val ADD = 1 // add triggers for this table
const val REMOVE = 2 // remove triggers for this table
}
}
/**
* An Observer wrapper that keeps a weak reference to the given object.
*
* This class will automatically unsubscribe when the wrapped observer goes out of memory.
*/
internal class WeakObserver(
val tracker: InvalidationTracker,
delegate: Observer
) : Observer(delegate.tables) {
val delegateRef: WeakReference<Observer> = WeakReference(delegate)
override fun onInvalidated(tables: Set<String>) {
val observer = delegateRef.get()
if (observer == null) {
tracker.removeObserver(this)
} else {
observer.onInvalidated(tables)
}
}
}
companion object {
private val TRIGGERS = arrayOf("UPDATE", "DELETE", "INSERT")
private const val UPDATE_TABLE_NAME = "room_table_modification_log"
private const val TABLE_ID_COLUMN_NAME = "table_id"
private const val INVALIDATED_COLUMN_NAME = "invalidated"
private const val CREATE_TRACKING_TABLE_SQL =
"CREATE TEMP TABLE $UPDATE_TABLE_NAME ($TABLE_ID_COLUMN_NAME INTEGER PRIMARY KEY, " +
"$INVALIDATED_COLUMN_NAME INTEGER NOT NULL DEFAULT 0)"
@VisibleForTesting
internal const val RESET_UPDATED_TABLES_SQL =
"UPDATE $UPDATE_TABLE_NAME SET $INVALIDATED_COLUMN_NAME = 0 " +
"WHERE $INVALIDATED_COLUMN_NAME = 1"
@VisibleForTesting
internal const val SELECT_UPDATED_TABLES_SQL =
"SELECT * FROM $UPDATE_TABLE_NAME WHERE $INVALIDATED_COLUMN_NAME = 1;"
internal fun getTriggerName(
tableName: String,
triggerType: String
) = "`room_table_modification_trigger_${tableName}_$triggerType`"
internal fun beginTransactionInternal(database: SupportSQLiteDatabase) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
database.isWriteAheadLoggingEnabled
) {
database.beginTransactionNonExclusive()
} else {
database.beginTransaction()
}
}
}
}