Merge "Delete unused JDBCOpenHelper" into oc-mr1-support-27.0-dev
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/MediatorLiveData.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/MediatorLiveData.java
index 672b3a3..718c79e 100644
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/MediatorLiveData.java
+++ b/lifecycle/extensions/src/main/java/android/arch/lifecycle/MediatorLiveData.java
@@ -24,11 +24,43 @@
import java.util.Map;
/**
- * {@link LiveData} subclass which may observer other {@code LiveData} objects and react on
+ * {@link LiveData} subclass which may observe other {@code LiveData} objects and react on
* {@code OnChanged} events from them.
* <p>
* This class correctly propagates its active/inactive states down to source {@code LiveData}
* objects.
+ * <p>
+ * Consider the following scenario: we have 2 instances of {@code LiveData}, let's name them
+ * {@code liveData1} and {@code liveData2}, and we want to merge their emissions in one object:
+ * {@code liveDataMerger}. Then, {@code liveData1} and {@code liveData2} will become sources for
+ * the {@code MediatorLiveData liveDataMerger} and every time {@code onChanged} callback
+ * is called for either of them, we set a new value in {@code liveDataMerger}.
+ *
+ * <pre>
+ * LiveData<Integer> liveData1 = ...;
+ * LiveData<Integer> liveData2 = ...;
+ *
+ * MediatorLiveData<Integer> liveDataMerger = new MediatorLiveData<>();
+ * liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
+ * liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
+ * </pre>
+ * <p>
+ * Let's consider that we only want 10 values emitted by {@code liveData1}, to be
+ * merged in the {@code liveDataMerger}. Then, after 10 values, we can stop listening to {@code
+ * liveData1} and remove it as a source.
+ * <pre>
+ * liveDataMerger.addSource(liveData1, new Observer<Integer>() {
+ * private int count = 1;
+ *
+ * {@literal @}Override public void onChanged(@Nullable Integer s) {
+ * count++;
+ * liveDataMerger.setValue(s);
+ * if (count > 10) {
+ * liveDataMerger.removeSource(liveData1);
+ * }
+ * }
+ * });
+ * </pre>
*
* @param <T> The type of data hold by this instance
*/
diff --git a/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java b/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java
index c64be96..f05e6be 100644
--- a/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java
+++ b/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java
@@ -125,4 +125,12 @@
* annotation.
*/
public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR";
+
+ /**
+ * Reported when a @Query method returns a Pojo that has relations but the method is not
+ * annotated with @Transaction. Relations are run as separate queries and if the query is not
+ * run inside a transaction, it might return inconsistent results from the database.
+ */
+ public static final String RELATION_QUERY_WITHOUT_TRANSACTION =
+ "ROOM_RELATION_QUERY_WITHOUT_TRANSACTION";
}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Transaction.java b/room/common/src/main/java/android/arch/persistence/room/Transaction.java
index 914e4f4..3b6ede9 100644
--- a/room/common/src/main/java/android/arch/persistence/room/Transaction.java
+++ b/room/common/src/main/java/android/arch/persistence/room/Transaction.java
@@ -22,9 +22,10 @@
import java.lang.annotation.Target;
/**
- * Marks a method in an abstract {@link Dao} class as a transaction method.
+ * Marks a method in a {@link Dao} class as a transaction method.
* <p>
- * The derived implementation of the method will execute the super method in a database transaction.
+ * When used on a non-abstract method of an abstract {@link Dao} class,
+ * the derived implementation of the method will execute the super method in a database transaction.
* All the parameters and return types are preserved. The transaction will be marked as successful
* unless an exception is thrown in the method body.
* <p>
@@ -44,6 +45,38 @@
* }
* }
* </pre>
+ * <p>
+ * When used on a {@link Query} method that has a {@code Select} statement, the generated code for
+ * the Query will be run in a transaction. There are 2 main cases where you may want to do that:
+ * <ol>
+ * <li>If the result of the query is fairly big, it is better to run it inside a transaction
+ * to receive a consistent result. Otherwise, if the query result does not fit into a single
+ * {@link android.database.CursorWindow CursorWindow}, the query result may be corrupted due to
+ * changes in the database in between cursor window swaps.
+ * <li>If the result of the query is a Pojo with {@link Relation} fields, these fields are
+ * queried separately. To receive consistent results between these queries, you probably want
+ * to run them in a single transaction.
+ * </ol>
+ * Example:
+ * <pre>
+ * class ProductWithReviews extends Product {
+ * {@literal @}Relation(parentColumn = "id", entityColumn = "productId", entity = Review.class)
+ * public List<Review> reviews;
+ * }
+ * {@literal @}Dao
+ * public interface ProductDao {
+ * {@literal @}Transaction {@literal @}Query("SELECT * from products")
+ * public List<ProductWithReviews> loadAll();
+ * }
+ * </pre>
+ * If the query is an async query (e.g. returns a {@link android.arch.lifecycle.LiveData LiveData}
+ * or RxJava Flowable, the transaction is properly handled when the query is run, not when the
+ * method is called.
+ * <p>
+ * Putting this annotation on an {@link Insert}, {@link Update} or {@link Delete} method has no
+ * impact because they are always run inside a transaction. Similarly, if it is annotated with
+ * {@link Query} but runs an update or delete statement, it is automatically wrapped in a
+ * transaction.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
index 2093d91..c705eef 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
@@ -110,9 +110,10 @@
executableElement = it).process()
} ?: emptyList()
- val transactionMethods = allMembers.filter {
- it.hasAnnotation(Transaction::class)
- && it.kind == ElementKind.METHOD
+ val transactionMethods = allMembers.filter { member ->
+ member.hasAnnotation(Transaction::class)
+ && member.kind == ElementKind.METHOD
+ && PROCESSED_ANNOTATIONS.none { member.hasAnnotation(it) }
// TODO: Exclude abstract methods and let @Query handle that case
}.map {
TransactionMethodProcessor(
@@ -161,5 +162,4 @@
element.toString(), dbType.toString()))
}
}
-
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
index be26297..4752478 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
@@ -37,6 +37,8 @@
val INVALID_ON_CONFLICT_VALUE = "On conflict value must be one of @OnConflictStrategy values."
val INVALID_INSERTION_METHOD_RETURN_TYPE = "Methods annotated with @Insert can return either" +
" void, long, Long, long[], Long[] or List<Long>."
+ val TRANSACTION_REFERENCE_DOCS = "https://developer.android.com/reference/android/arch/" +
+ "persistence/room/Transaction.html"
fun insertionMethodReturnTypeMismatch(definedReturn : TypeName,
expectedReturnTypes : List<TypeName>) : String {
@@ -122,7 +124,13 @@
" @Update but does not have any parameters to update."
val TRANSACTION_METHOD_MODIFIERS = "Method annotated with @Transaction must not be " +
- "private, final, or abstract."
+ "private, final, or abstract. It can be abstract only if the method is also" +
+ " annotated with @Query."
+
+ val TRANSACTION_MISSING_ON_RELATION = "The return value includes a Pojo with a @Relation." +
+ " It is usually desired to annotate this method with @Transaction to avoid" +
+ " possibility of inconsistent results between the Pojo and its relations. See " +
+ TRANSACTION_REFERENCE_DOCS + " for details."
val CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER = "Type of the parameter must be a class " +
"annotated with @Entity or a collection/array of it."
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt
index fc60074..ebd586a 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt
@@ -18,15 +18,18 @@
import android.arch.persistence.room.Query
import android.arch.persistence.room.SkipQueryVerification
+import android.arch.persistence.room.Transaction
import android.arch.persistence.room.ext.hasAnnotation
import android.arch.persistence.room.parser.ParsedQuery
import android.arch.persistence.room.parser.QueryType
import android.arch.persistence.room.parser.SqlParser
import android.arch.persistence.room.solver.query.result.LiveDataQueryResultBinder
+import android.arch.persistence.room.solver.query.result.PojoRowAdapter
import android.arch.persistence.room.verifier.DatabaseVerificaitonErrors
import android.arch.persistence.room.verifier.DatabaseVerifier
import android.arch.persistence.room.vo.QueryMethod
import android.arch.persistence.room.vo.QueryParameter
+import android.arch.persistence.room.vo.Warning
import com.google.auto.common.AnnotationMirrors
import com.google.auto.common.MoreElements
import com.google.auto.common.MoreTypes
@@ -91,6 +94,22 @@
ProcessorErrors.LIVE_DATA_QUERY_WITHOUT_SELECT)
}
+ val inTransaction = when (query.type) {
+ QueryType.SELECT -> executableElement.hasAnnotation(Transaction::class)
+ else -> true
+ }
+
+ if (query.type == QueryType.SELECT && !inTransaction) {
+ // put a warning if it is has relations and not annotated w/ transaction
+ resultBinder.adapter?.rowAdapter?.let { rowAdapter ->
+ if (rowAdapter is PojoRowAdapter
+ && rowAdapter.relationCollectors.isNotEmpty()) {
+ context.logger.w(Warning.RELATION_QUERY_WITHOUT_TRANSACTION,
+ executableElement, ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
+ }
+ }
+ }
+
val queryMethod = QueryMethod(
element = executableElement,
query = query,
@@ -101,6 +120,7 @@
baseContext = context,
containing = containing,
element = it).process() },
+ inTransaction = inTransaction,
queryResultBinder = resultBinder)
val missing = queryMethod.sectionToParamMapping
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
index f861ebe..256304a 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
@@ -20,9 +20,9 @@
import android.arch.persistence.room.vo.TransactionMethod
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier.ABSTRACT
-import javax.lang.model.type.DeclaredType
import javax.lang.model.element.Modifier.FINAL
import javax.lang.model.element.Modifier.PRIVATE
+import javax.lang.model.type.DeclaredType
class TransactionMethodProcessor(baseContext: Context,
val containing: DeclaredType,
@@ -31,7 +31,6 @@
val context = baseContext.fork(executableElement)
fun process(): TransactionMethod {
- // TODO: Remove abstract check
context.checker.check(!executableElement.hasAnyOf(PRIVATE, FINAL, ABSTRACT),
executableElement, ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
index 84c71eb..0bd3fb1 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
@@ -38,8 +38,7 @@
override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
val typeArg = declared.typeArguments.first()
val adapter = context.typeAdapterStore.findQueryResultAdapter(typeArg, query)
- return RxCallableQueryResultBinder(rxType,
- typeArg, InstantQueryResultBinder(adapter), adapter)
+ return RxCallableQueryResultBinder(rxType, typeArg, adapter)
}
override fun matches(declared: DeclaredType): Boolean =
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt
index 7872c33..883180c 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt
@@ -22,6 +22,7 @@
import android.arch.persistence.room.ext.T
import android.arch.persistence.room.solver.CodeGenScope
import android.arch.persistence.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.MethodSpec
import javax.lang.model.element.Modifier
@@ -42,9 +43,17 @@
protected fun createRunQueryAndReturnStatements(builder: MethodSpec.Builder,
roomSQLiteQueryVar: String,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
scope: CodeGenScope) {
+ val transactionWrapper = if (inTransaction) {
+ builder.transactionWrapper(dbField)
+ } else {
+ null
+ }
val outVar = scope.getTmpVar("_result")
val cursorVar = scope.getTmpVar("_cursor")
+ transactionWrapper?.beginTransactionWithControlFlow()
builder.apply {
addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
DaoWriter.dbField, roomSQLiteQueryVar)
@@ -52,6 +61,7 @@
val adapterScope = scope.fork()
adapter?.convert(outVar, cursorVar, adapterScope)
addCode(adapterScope.builder().build())
+ transactionWrapper?.commitTransaction()
addStatement("return $L", outVar)
}
nextControlFlow("finally").apply {
@@ -59,5 +69,6 @@
}
endControlFlow()
}
+ transactionWrapper?.endTransactionWithControlFlow()
}
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
index cd8ea74..06ec339 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
@@ -16,19 +16,34 @@
package android.arch.persistence.room.solver.query.result
+import android.arch.persistence.room.ext.AndroidTypeNames
import android.arch.persistence.room.ext.L
import android.arch.persistence.room.ext.N
+import android.arch.persistence.room.ext.T
import android.arch.persistence.room.solver.CodeGenScope
import android.arch.persistence.room.writer.DaoWriter
import com.squareup.javapoet.FieldSpec
class CursorQueryResultBinder : QueryResultBinder(NO_OP_RESULT_ADAPTER) {
- override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+ override fun convertAndReturn(roomSQLiteQueryVar: String,
+ dbField: FieldSpec,
+ inTransaction : Boolean,
scope: CodeGenScope) {
- scope.builder().apply {
- addStatement("return $N.query($L)", DaoWriter.dbField, roomSQLiteQueryVar)
+ val builder = scope.builder()
+ val transactionWrapper = if (inTransaction) {
+ builder.transactionWrapper(dbField)
+ } else {
+ null
}
+ transactionWrapper?.beginTransactionWithControlFlow()
+ val resultName = scope.getTmpVar("_tmpResult")
+ builder.addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, resultName,
+ dbField, roomSQLiteQueryVar)
+ transactionWrapper?.commitTransaction()
+ builder.addStatement("return $L", resultName)
+ transactionWrapper?.endTransactionWithControlFlow()
}
+
companion object {
private val NO_OP_RESULT_ADAPTER = object : QueryResultAdapter(null) {
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
index 5c8e3e7..e1a75b5 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
@@ -37,7 +37,9 @@
class FlowableQueryResultBinder(val typeArg: TypeMirror, val queryTableNames: Set<String>,
adapter: QueryResultAdapter?)
: BaseObservableQueryResultBinder(adapter) {
- override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+ override fun convertAndReturn(roomSQLiteQueryVar: String,
+ dbField: FieldSpec,
+ inTransaction : Boolean,
scope: CodeGenScope) {
val callableImpl = TypeSpec.anonymousClassBuilder("").apply {
val typeName = typeArg.typeName()
@@ -47,7 +49,11 @@
returns(typeName)
addException(Exception::class.typeName())
addModifiers(Modifier.PUBLIC)
- createRunQueryAndReturnStatements(this, roomSQLiteQueryVar, scope)
+ createRunQueryAndReturnStatements(builder = this,
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ inTransaction = inTransaction,
+ dbField = dbField,
+ scope = scope)
}.build())
addMethod(createFinalizeMethod(roomSQLiteQueryVar))
}.build()
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
index ff15252..c18bf2d 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
@@ -27,8 +27,16 @@
* Instantly runs and returns the query.
*/
class InstantQueryResultBinder(adapter: QueryResultAdapter?) : QueryResultBinder(adapter) {
- override fun convertAndReturn(roomSQLiteQueryVar : String, dbField: FieldSpec,
+ override fun convertAndReturn(roomSQLiteQueryVar : String,
+ dbField: FieldSpec,
+ inTransaction : Boolean,
scope: CodeGenScope) {
+ val transactionWrapper = if (inTransaction) {
+ scope.builder().transactionWrapper(dbField)
+ } else {
+ null
+ }
+ transactionWrapper?.beginTransactionWithControlFlow()
scope.builder().apply {
val outVar = scope.getTmpVar("_result")
val cursorVar = scope.getTmpVar("_cursor")
@@ -36,6 +44,7 @@
DaoWriter.dbField, roomSQLiteQueryVar)
beginControlFlow("try").apply {
adapter?.convert(outVar, cursorVar, scope)
+ transactionWrapper?.commitTransaction()
addStatement("return $L", outVar)
}
nextControlFlow("finally").apply {
@@ -44,5 +53,6 @@
}
endControlFlow()
}
+ transactionWrapper?.endTransactionWithControlFlow()
}
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
index a385a94..0ef8a93 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
@@ -40,7 +40,9 @@
adapter: QueryResultAdapter?)
: BaseObservableQueryResultBinder(adapter) {
@Suppress("JoinDeclarationAndAssignment")
- override fun convertAndReturn(roomSQLiteQueryVar : String, dbField: FieldSpec,
+ override fun convertAndReturn(roomSQLiteQueryVar : String,
+ dbField: FieldSpec,
+ inTransaction : Boolean,
scope: CodeGenScope) {
val typeName = typeArg.typeName()
@@ -55,6 +57,7 @@
typeName = typeName,
roomSQLiteQueryVar = roomSQLiteQueryVar,
dbField = dbField,
+ inTransaction = inTransaction,
scope = scope
))
addMethod(createFinalizeMethod(roomSQLiteQueryVar))
@@ -66,6 +69,7 @@
private fun createComputeMethod(roomSQLiteQueryVar: String, typeName: TypeName,
observerField: FieldSpec, dbField: FieldSpec,
+ inTransaction: Boolean,
scope: CodeGenScope): MethodSpec {
return MethodSpec.methodBuilder("compute").apply {
addAnnotation(Override::class.java)
@@ -79,7 +83,11 @@
}
endControlFlow()
- createRunQueryAndReturnStatements(this, roomSQLiteQueryVar, scope)
+ createRunQueryAndReturnStatements(builder = this,
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ dbField = dbField,
+ inTransaction = inTransaction,
+ scope = scope)
}.build()
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt
index 39ea32b..c10f9fb 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt
@@ -31,14 +31,20 @@
: QueryResultBinder(tiledDataSourceQueryResultBinder.listAdapter) {
@Suppress("HasPlatformType")
val typeName = tiledDataSourceQueryResultBinder.itemTypeName
- override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+ override fun convertAndReturn(roomSQLiteQueryVar: String,
+ dbField: FieldSpec,
+ inTransaction : Boolean,
scope: CodeGenScope) {
scope.builder().apply {
val pagedListProvider = TypeSpec
.anonymousClassBuilder("").apply {
superclass(ParameterizedTypeName.get(PagingTypeNames.LIVE_PAGED_LIST_PROVIDER,
Integer::class.typeName(), typeName))
- addMethod(createCreateDataSourceMethod(roomSQLiteQueryVar, dbField, scope))
+ addMethod(createCreateDataSourceMethod(
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ dbField = dbField,
+ inTransaction = inTransaction,
+ scope = scope))
}.build()
addStatement("return $L", pagedListProvider)
}
@@ -46,14 +52,18 @@
private fun createCreateDataSourceMethod(roomSQLiteQueryVar: String,
dbField: FieldSpec,
+ inTransaction : Boolean,
scope: CodeGenScope): MethodSpec
= MethodSpec.methodBuilder("createDataSource").apply {
addAnnotation(Override::class.java)
addModifiers(Modifier.PROTECTED)
returns(tiledDataSourceQueryResultBinder.typeName)
val countedBinderScope = scope.fork()
- tiledDataSourceQueryResultBinder.convertAndReturn(roomSQLiteQueryVar, dbField,
- countedBinderScope)
+ tiledDataSourceQueryResultBinder.convertAndReturn(
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ dbField = dbField,
+ inTransaction = inTransaction,
+ scope = countedBinderScope)
addCode(countedBinderScope.builder().build())
}.build()
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
index 205bd88..652de46 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
@@ -32,5 +32,7 @@
* and returns the result.
*/
abstract fun convertAndReturn(roomSQLiteQueryVar: String,
- dbField: FieldSpec, scope: CodeGenScope)
+ dbField: FieldSpec,
+ inTransaction : Boolean,
+ scope: CodeGenScope)
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
index da960f6..1ab91e8 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
@@ -38,28 +38,42 @@
*/
class RxCallableQueryResultBinder(val rxType: RxType,
val typeArg: TypeMirror,
- val instantBinder : InstantQueryResultBinder,
- adapter: QueryResultAdapter?) : QueryResultBinder(adapter) {
- override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+ adapter: QueryResultAdapter?)
+ : QueryResultBinder(adapter) {
+ override fun convertAndReturn(roomSQLiteQueryVar: String,
+ dbField: FieldSpec,
+ inTransaction : Boolean,
scope: CodeGenScope) {
val callable = TypeSpec.anonymousClassBuilder("").apply {
val typeName = typeArg.typeName()
superclass(ParameterizedTypeName.get(java.util.concurrent.Callable::class.typeName(),
typeName))
- addMethod(createCallMethod(roomSQLiteQueryVar, dbField, scope))
+ addMethod(createCallMethod(
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ dbField = dbField,
+ inTransaction = inTransaction,
+ scope = scope))
}.build()
scope.builder().apply {
addStatement("return $T.fromCallable($L)", rxType.className, callable)
}
}
- fun createCallMethod(roomSQLiteQueryVar: String, dbField: FieldSpec,
+ fun createCallMethod(roomSQLiteQueryVar: String,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
scope: CodeGenScope): MethodSpec {
val adapterScope = scope.fork()
return MethodSpec.methodBuilder("call").apply {
returns(typeArg.typeName())
addException(Exception::class.typeName())
addModifiers(Modifier.PUBLIC)
+ val transactionWrapper = if (inTransaction) {
+ transactionWrapper(dbField)
+ } else {
+ null
+ }
+ transactionWrapper?.beginTransactionWithControlFlow()
val outVar = scope.getTmpVar("_result")
val cursorVar = scope.getTmpVar("_cursor")
addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
@@ -76,6 +90,7 @@
}
endControlFlow()
}
+ transactionWrapper?.commitTransaction()
addStatement("return $L", outVar)
}
nextControlFlow("finally").apply {
@@ -83,10 +98,11 @@
addStatement("$L.release()", roomSQLiteQueryVar)
}
endControlFlow()
+ transactionWrapper?.endTransactionWithControlFlow()
}.build()
}
- enum class RxType(val className : ClassName, val canBeNull : Boolean) {
+ enum class RxType(val className: ClassName, val canBeNull: Boolean) {
SINGLE(RxJava2TypeNames.SINGLE, canBeNull = false),
MAYBE(RxJava2TypeNames.MAYBE, canBeNull = true);
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt
index 1b4b3fe..2281cfb 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt
@@ -37,11 +37,13 @@
val itemTypeName : TypeName = listAdapter?.rowAdapter?.out?.typeName() ?: TypeName.OBJECT
val typeName : ParameterizedTypeName = ParameterizedTypeName.get(
RoomTypeNames.LIMIT_OFFSET_DATA_SOURCE, itemTypeName)
- override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+ override fun convertAndReturn(roomSQLiteQueryVar: String,
+ dbField: FieldSpec,
+ inTransaction : Boolean,
scope: CodeGenScope) {
val tableNamesList = tableNames.joinToString(",") { "\"$it\"" }
- val spec = TypeSpec.anonymousClassBuilder("$N, $L, $L",
- dbField, roomSQLiteQueryVar, tableNamesList).apply {
+ val spec = TypeSpec.anonymousClassBuilder("$N, $L, $L, $L",
+ dbField, roomSQLiteQueryVar, inTransaction, tableNamesList).apply {
superclass(typeName)
addMethod(createConvertRowsMethod(scope))
}.build()
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TransactionWrapper.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TransactionWrapper.kt
new file mode 100644
index 0000000..30d02ee
--- /dev/null
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TransactionWrapper.kt
@@ -0,0 +1,65 @@
+/*
+ * 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 android.arch.persistence.room.solver.query.result
+
+import android.arch.persistence.room.ext.N
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+
+/**
+ * helper class to create correct transaction code.
+ */
+interface TransactionWrapper {
+ fun beginTransactionWithControlFlow()
+ fun commitTransaction()
+ fun endTransactionWithControlFlow()
+}
+
+fun MethodSpec.Builder.transactionWrapper(dbField : FieldSpec) = object : TransactionWrapper {
+ override fun beginTransactionWithControlFlow() {
+ addStatement("$N.beginTransaction()", dbField)
+ beginControlFlow("try")
+ }
+
+ override fun commitTransaction() {
+ addStatement("$N.setTransactionSuccessful()", dbField)
+ }
+
+ override fun endTransactionWithControlFlow() {
+ nextControlFlow("finally")
+ addStatement("$N.endTransaction()", dbField)
+ endControlFlow()
+ }
+}
+
+fun CodeBlock.Builder.transactionWrapper(dbField: FieldSpec) = object : TransactionWrapper {
+ override fun beginTransactionWithControlFlow() {
+ addStatement("$N.beginTransaction()", dbField)
+ beginControlFlow("try")
+ }
+
+ override fun commitTransaction() {
+ addStatement("$N.setTransactionSuccessful()", dbField)
+ }
+
+ override fun endTransactionWithControlFlow() {
+ nextControlFlow("finally")
+ addStatement("$N.endTransaction()", dbField)
+ endControlFlow()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt
index 1aa8430..a6b8cb7 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt
@@ -29,6 +29,7 @@
*/
data class QueryMethod(val element: ExecutableElement, val query: ParsedQuery, val name: String,
val returnType: TypeMirror, val parameters: List<QueryParameter>,
+ val inTransaction : Boolean,
val queryResultBinder : QueryResultBinder) {
val sectionToParamMapping by lazy {
query.bindSections.map {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt
index 91bcb33..dd154c4 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt
@@ -32,6 +32,7 @@
RELATION_TYPE_MISMATCH("ROOM_RELATION_TYPE_MISMATCH"),
MISSING_SCHEMA_LOCATION("ROOM_MISSING_SCHEMA_LOCATION"),
MISSING_INDEX_ON_FOREIGN_KEY_CHILD("ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX"),
+ RELATION_QUERY_WITHOUT_TRANSACTION("ROOM_RELATION_QUERY_WITHOUT_TRANSACTION"),
DEFAULT_CONSTRUCTOR("ROOM_DEFAULT_CONSTRUCTOR");
companion object {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
index 5c7d8a3..0bb9c19 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
@@ -445,7 +445,11 @@
val sqlVar = scope.getTmpVar("_sql")
val roomSQLiteQueryVar = scope.getTmpVar("_statement")
queryWriter.prepareReadAndBind(sqlVar, roomSQLiteQueryVar, scope)
- method.queryResultBinder.convertAndReturn(roomSQLiteQueryVar, dbField, scope)
+ method.queryResultBinder.convertAndReturn(
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ dbField = dbField,
+ inTransaction = method.inTransaction,
+ scope = scope)
return scope.builder().build()
}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt
index 3ade277..55f335d 100644
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt
@@ -192,6 +192,81 @@
}.compilesWithoutError()
}
+ @Test
+ fun query_warnIfTransactionIsMissingForRelation() {
+ if (!enableVerification) {
+ return
+ }
+ singleDao(
+ """
+ @Dao interface MyDao {
+ static class Merged extends User {
+ @Relation(parentColumn = "name", entityColumn = "lastName",
+ entity = User.class)
+ java.util.List<User> users;
+ }
+ @Query("select * from user")
+ abstract java.util.List<Merged> loadUsers();
+ }
+ """
+ ) { dao, _ ->
+ assertThat(dao.queryMethods.size, `is`(1))
+ assertThat(dao.queryMethods.first().inTransaction, `is`(false))
+ }.compilesWithoutError()
+ .withWarningContaining(ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
+ }
+
+ @Test
+ fun query_dontWarnIfTransactionIsMissingForRelation_suppressed() {
+ if (!enableVerification) {
+ return
+ }
+ singleDao(
+ """
+ @Dao interface MyDao {
+ static class Merged extends User {
+ @Relation(parentColumn = "name", entityColumn = "lastName",
+ entity = User.class)
+ java.util.List<User> users;
+ }
+ @SuppressWarnings(RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION)
+ @Query("select * from user")
+ abstract java.util.List<Merged> loadUsers();
+ }
+ """
+ ) { dao, _ ->
+ assertThat(dao.queryMethods.size, `is`(1))
+ assertThat(dao.queryMethods.first().inTransaction, `is`(false))
+ }.compilesWithoutError()
+ .withWarningCount(0)
+ }
+
+ @Test
+ fun query_dontWarnIfTransactionNotIsMissingForRelation() {
+ if (!enableVerification) {
+ return
+ }
+ singleDao(
+ """
+ @Dao interface MyDao {
+ static class Merged extends User {
+ @Relation(parentColumn = "name", entityColumn = "lastName",
+ entity = User.class)
+ java.util.List<User> users;
+ }
+ @Transaction
+ @Query("select * from user")
+ abstract java.util.List<Merged> loadUsers();
+ }
+ """
+ ) { dao, _ ->
+ // test sanity
+ assertThat(dao.queryMethods.size, `is`(1))
+ assertThat(dao.queryMethods.first().inTransaction, `is`(true))
+ }.compilesWithoutError()
+ .withWarningCount(0)
+ }
+
fun singleDao(vararg inputs: String, handler: (Dao, TestInvocation) -> Unit):
CompileTester {
return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
@@ -200,7 +275,12 @@
), COMMON.USER))
.processedWith(TestProcessor.builder()
.forAnnotations(android.arch.persistence.room.Dao::class,
- android.arch.persistence.room.Entity::class)
+ android.arch.persistence.room.Entity::class,
+ android.arch.persistence.room.Relation::class,
+ android.arch.persistence.room.Transaction::class,
+ android.arch.persistence.room.ColumnInfo::class,
+ android.arch.persistence.room.PrimaryKey::class,
+ android.arch.persistence.room.Query::class)
.nextRunHandler { invocation ->
val dao = invocation.roundEnv
.getElementsAnnotatedWith(
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt
index a2cb768..e07e04d 100644
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt
@@ -445,6 +445,55 @@
}
@Test
+ fun query_detectTransaction_delete() {
+ singleQueryMethod(
+ """
+ @Query("delete from user where uid = :id")
+ abstract int deleteUser(String id);
+ """
+ ) { method, _ ->
+ assertThat(method.inTransaction, `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun query_detectTransaction_update() {
+ singleQueryMethod(
+ """
+ @Query("UPDATE user set uid = :id + 1 where uid = :id")
+ abstract int incrementId(String id);
+ """
+ ) { method, _ ->
+ assertThat(method.inTransaction, `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun query_detectTransaction_select() {
+ singleQueryMethod(
+ """
+ @Query("select * from user")
+ abstract int loadUsers();
+ """
+ ) { method, _ ->
+ assertThat(method.inTransaction, `is`(false))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun query_detectTransaction_selectInTransaction() {
+ singleQueryMethod(
+ """
+ @Transaction
+ @Query("select * from user")
+ abstract int loadUsers();
+ """
+ ) { method, _ ->
+ assertThat(method.inTransaction, `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
fun skipVerification() {
singleQueryMethod(
"""
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
new file mode 100644
index 0000000..dcf98c9
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
@@ -0,0 +1,471 @@
+/*
+ * 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 android.arch.persistence.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.core.executor.ArchTaskExecutor;
+import android.arch.core.executor.testing.CountingTaskExecutorRule;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.Observer;
+import android.arch.paging.LivePagedListProvider;
+import android.arch.paging.PagedList;
+import android.arch.paging.TiledDataSource;
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Database;
+import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.Ignore;
+import android.arch.persistence.room.Insert;
+import android.arch.persistence.room.PrimaryKey;
+import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Relation;
+import android.arch.persistence.room.Room;
+import android.arch.persistence.room.RoomDatabase;
+import android.arch.persistence.room.RoomWarnings;
+import android.arch.persistence.room.Transaction;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.reactivex.Flowable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.reactivex.observers.TestObserver;
+import io.reactivex.schedulers.Schedulers;
+import io.reactivex.subscribers.TestSubscriber;
+
+@SmallTest
+@RunWith(Parameterized.class)
+public class QueryTransactionTest {
+ @Rule
+ public CountingTaskExecutorRule countingTaskExecutorRule = new CountingTaskExecutorRule();
+ private static final AtomicInteger sStartedTransactionCount = new AtomicInteger(0);
+ private TransactionDb mDb;
+ private final boolean mUseTransactionDao;
+ private Entity1Dao mDao;
+ private final LiveDataQueryTest.TestLifecycleOwner mLifecycleOwner = new LiveDataQueryTest
+ .TestLifecycleOwner();
+
+ @NonNull
+ @Parameterized.Parameters(name = "useTransaction_{0}")
+ public static Boolean[] getParams() {
+ return new Boolean[]{false, true};
+ }
+
+ public QueryTransactionTest(boolean useTransactionDao) {
+ mUseTransactionDao = useTransactionDao;
+ }
+
+ @Before
+ public void initDb() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ }
+ });
+
+ resetTransactionCount();
+ mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
+ TransactionDb.class).build();
+ mDao = mUseTransactionDao ? mDb.transactionDao() : mDb.dao();
+ drain();
+ }
+
+ @After
+ public void closeDb() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mLifecycleOwner.handleEvent(Lifecycle.Event.ON_DESTROY);
+ }
+ });
+ drain();
+ mDb.close();
+ }
+
+ @Test
+ public void readList() {
+ mDao.insert(new Entity1(1, "foo"));
+ resetTransactionCount();
+
+ int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+ List<Entity1> allEntities = mDao.allEntities();
+ assertTransactionCount(allEntities, expectedTransactionCount);
+ }
+
+ @Test
+ public void liveData() {
+ LiveData<List<Entity1>> listLiveData = mDao.liveData();
+ observeForever(listLiveData);
+ drain();
+ assertThat(listLiveData.getValue(), is(Collections.<Entity1>emptyList()));
+
+ resetTransactionCount();
+ mDao.insert(new Entity1(1, "foo"));
+ drain();
+
+ //noinspection ConstantConditions
+ assertThat(listLiveData.getValue().size(), is(1));
+ int expectedTransactionCount = mUseTransactionDao ? 2 : 1;
+ assertTransactionCount(listLiveData.getValue(), expectedTransactionCount);
+ }
+
+ @Test
+ public void flowable() {
+ Flowable<List<Entity1>> flowable = mDao.flowable();
+ TestSubscriber<List<Entity1>> subscriber = observe(flowable);
+ drain();
+ assertThat(subscriber.values().size(), is(1));
+
+ resetTransactionCount();
+ mDao.insert(new Entity1(1, "foo"));
+ drain();
+
+ List<Entity1> allEntities = subscriber.values().get(1);
+ assertThat(allEntities.size(), is(1));
+ int expectedTransactionCount = mUseTransactionDao ? 2 : 1;
+ assertTransactionCount(allEntities, expectedTransactionCount);
+ }
+
+ @Test
+ public void maybe() {
+ mDao.insert(new Entity1(1, "foo"));
+ resetTransactionCount();
+
+ int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+ Maybe<List<Entity1>> listMaybe = mDao.maybe();
+ TestObserver<List<Entity1>> observer = observe(listMaybe);
+ drain();
+ List<Entity1> allEntities = observer.values().get(0);
+ assertTransactionCount(allEntities, expectedTransactionCount);
+ }
+
+ @Test
+ public void single() {
+ mDao.insert(new Entity1(1, "foo"));
+ resetTransactionCount();
+
+ int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+ Single<List<Entity1>> listMaybe = mDao.single();
+ TestObserver<List<Entity1>> observer = observe(listMaybe);
+ drain();
+ List<Entity1> allEntities = observer.values().get(0);
+ assertTransactionCount(allEntities, expectedTransactionCount);
+ }
+
+ @Test
+ public void relation() {
+ mDao.insert(new Entity1(1, "foo"));
+ mDao.insert(new Child(1, 1));
+ mDao.insert(new Child(2, 1));
+ resetTransactionCount();
+
+ List<Entity1WithChildren> result = mDao.withRelation();
+ int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+ assertTransactionCountWithChildren(result, expectedTransactionCount);
+ }
+
+ @Test
+ public void pagedList() {
+ LiveData<PagedList<Entity1>> pagedList = mDao.pagedList().create(null, 10);
+ observeForever(pagedList);
+ drain();
+ assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
+
+ mDao.insert(new Entity1(1, "foo"));
+ drain();
+ //noinspection ConstantConditions
+ assertThat(pagedList.getValue().size(), is(1));
+ assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 3 : 1);
+
+ mDao.insert(new Entity1(2, "bar"));
+ drain();
+ assertThat(pagedList.getValue().size(), is(2));
+ assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 5 : 2);
+ }
+
+ @Test
+ public void dataSource() {
+ mDao.insert(new Entity1(2, "bar"));
+ drain();
+ resetTransactionCount();
+ TiledDataSource<Entity1> dataSource = mDao.dataSource();
+ dataSource.loadRange(0, 10);
+ assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
+ }
+
+ private void assertTransactionCount(List<Entity1> allEntities, int expectedTransactionCount) {
+ assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount));
+ assertThat(allEntities.isEmpty(), is(false));
+ for (Entity1 entity1 : allEntities) {
+ assertThat(entity1.transactionId, is(expectedTransactionCount));
+ }
+ }
+
+ private void assertTransactionCountWithChildren(List<Entity1WithChildren> allEntities,
+ int expectedTransactionCount) {
+ assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount));
+ assertThat(allEntities.isEmpty(), is(false));
+ for (Entity1WithChildren entity1 : allEntities) {
+ assertThat(entity1.transactionId, is(expectedTransactionCount));
+ assertThat(entity1.children, notNullValue());
+ assertThat(entity1.children.isEmpty(), is(false));
+ for (Child child : entity1.children) {
+ assertThat(child.transactionId, is(expectedTransactionCount));
+ }
+ }
+ }
+
+ private void resetTransactionCount() {
+ sStartedTransactionCount.set(0);
+ }
+
+ private void drain() {
+ try {
+ countingTaskExecutorRule.drainTasks(30, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new AssertionError("interrupted", e);
+ } catch (TimeoutException e) {
+ throw new AssertionError("drain timed out", e);
+ }
+ }
+
+ private <T> TestSubscriber<T> observe(final Flowable<T> flowable) {
+ TestSubscriber<T> subscriber = new TestSubscriber<>();
+ flowable.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+ .subscribeWith(subscriber);
+ return subscriber;
+ }
+
+ private <T> TestObserver<T> observe(final Maybe<T> maybe) {
+ TestObserver<T> observer = new TestObserver<>();
+ maybe.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+ .subscribeWith(observer);
+ return observer;
+ }
+
+ private <T> TestObserver<T> observe(final Single<T> single) {
+ TestObserver<T> observer = new TestObserver<>();
+ single.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+ .subscribeWith(observer);
+ return observer;
+ }
+
+ private <T> void observeForever(final LiveData<T> liveData) {
+ FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ liveData.observe(mLifecycleOwner, new Observer<T>() {
+ @Override
+ public void onChanged(@Nullable T t) {
+
+ }
+ });
+ return null;
+ }
+ });
+ ArchTaskExecutor.getMainThreadExecutor().execute(futureTask);
+ try {
+ futureTask.get();
+ } catch (InterruptedException e) {
+ throw new AssertionError("interrupted", e);
+ } catch (ExecutionException e) {
+ throw new AssertionError("execution error", e);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ static class Entity1WithChildren extends Entity1 {
+ @Relation(entity = Child.class, parentColumn = "id",
+ entityColumn = "entity1Id")
+ public List<Child> children;
+
+ Entity1WithChildren(int id, String value) {
+ super(id, value);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity
+ static class Child {
+ @PrimaryKey(autoGenerate = true)
+ public int id;
+ public int entity1Id;
+ @Ignore
+ public final int transactionId = sStartedTransactionCount.get();
+
+ Child(int id, int entity1Id) {
+ this.id = id;
+ this.entity1Id = entity1Id;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity
+ static class Entity1 {
+ @PrimaryKey(autoGenerate = true)
+ public int id;
+ public String value;
+ @Ignore
+ public final int transactionId = sStartedTransactionCount.get();
+
+ Entity1(int id, String value) {
+ this.id = id;
+ this.value = value;
+ }
+ }
+
+ // we don't support dao inheritance for queries so for now, go with this
+ interface Entity1Dao {
+ String SELECT_ALL = "select * from Entity1";
+
+ List<Entity1> allEntities();
+
+ Flowable<List<Entity1>> flowable();
+
+ Maybe<List<Entity1>> maybe();
+
+ Single<List<Entity1>> single();
+
+ LiveData<List<Entity1>> liveData();
+
+ List<Entity1WithChildren> withRelation();
+
+ LivePagedListProvider<Integer, Entity1> pagedList();
+
+ TiledDataSource<Entity1> dataSource();
+
+ @Insert
+ void insert(Entity1 entity1);
+
+ @Insert
+ void insert(Child entity1);
+ }
+
+ @Dao
+ interface EntityDao extends Entity1Dao {
+ @Override
+ @Query(SELECT_ALL)
+ List<Entity1> allEntities();
+
+ @Override
+ @Query(SELECT_ALL)
+ Flowable<List<Entity1>> flowable();
+
+ @Override
+ @Query(SELECT_ALL)
+ LiveData<List<Entity1>> liveData();
+
+ @Override
+ @Query(SELECT_ALL)
+ Maybe<List<Entity1>> maybe();
+
+ @Override
+ @Query(SELECT_ALL)
+ Single<List<Entity1>> single();
+
+ @Override
+ @Query(SELECT_ALL)
+ @SuppressWarnings(RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION)
+ List<Entity1WithChildren> withRelation();
+
+ @Override
+ @Query(SELECT_ALL)
+ LivePagedListProvider<Integer, Entity1> pagedList();
+
+ @Override
+ @Query(SELECT_ALL)
+ TiledDataSource<Entity1> dataSource();
+ }
+
+ @Dao
+ interface TransactionDao extends Entity1Dao {
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ List<Entity1> allEntities();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ Flowable<List<Entity1>> flowable();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ LiveData<List<Entity1>> liveData();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ Maybe<List<Entity1>> maybe();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ Single<List<Entity1>> single();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ List<Entity1WithChildren> withRelation();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ LivePagedListProvider<Integer, Entity1> pagedList();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ TiledDataSource<Entity1> dataSource();
+ }
+
+ @Database(version = 1, entities = {Entity1.class, Child.class}, exportSchema = false)
+ abstract static class TransactionDb extends RoomDatabase {
+ abstract EntityDao dao();
+
+ abstract TransactionDao transactionDao();
+
+ @Override
+ public void beginTransaction() {
+ super.beginTransaction();
+ sStartedTransactionCount.incrementAndGet();
+ }
+ }
+}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java b/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
index 800514c..2f9a888 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
@@ -49,10 +49,13 @@
private final RoomDatabase mDb;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationTracker.Observer mObserver;
+ private final boolean mInTransaction;
- protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query, String... tables) {
+ protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query,
+ boolean inTransaction, String... tables) {
mDb = db;
mSourceQuery = query;
+ mInTransaction = inTransaction;
mCountQuery = "SELECT COUNT(*) FROM ( " + mSourceQuery.getSql() + " )";
mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?";
mObserver = new InvalidationTracker.Observer(tables) {
@@ -98,13 +101,30 @@
sqLiteQuery.copyArgumentsFrom(mSourceQuery);
sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount);
sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition);
- Cursor cursor = mDb.query(sqLiteQuery);
-
- try {
- return convertRows(cursor);
- } finally {
- cursor.close();
- sqLiteQuery.release();
+ if (mInTransaction) {
+ mDb.beginTransaction();
+ Cursor cursor = null;
+ try {
+ cursor = mDb.query(sqLiteQuery);
+ List<T> 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();
+ }
}
}
}