Merge "Add @Transaction annotation" into oc-mr1-dev
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
new file mode 100644
index 0000000..914e4f4
--- /dev/null
+++ b/room/common/src/main/java/android/arch/persistence/room/Transaction.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a method in an abstract {@link Dao} class as a transaction method.
+ * <p>
+ * 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>
+ * Example:
+ * <pre>
+ * {@literal @}Dao
+ * public abstract class ProductDao {
+ * {@literal @}Insert
+ * public abstract void insert(Product product);
+ * {@literal @}Delete
+ * public abstract void delete(Product product);
+ * {@literal @}Transaction
+ * public void insertAndDeleteInTransaction(Product newProduct, Product oldProduct) {
+ * // Anything inside this method runs in a single transaction.
+ * insert(newProduct);
+ * delete(oldProduct);
+ * }
+ * }
+ * </pre>
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface Transaction {
+}
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 0c0ff49..2093d91 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
@@ -20,6 +20,7 @@
import android.arch.persistence.room.Insert
import android.arch.persistence.room.Query
import android.arch.persistence.room.SkipQueryVerification
+import android.arch.persistence.room.Transaction
import android.arch.persistence.room.Update
import android.arch.persistence.room.ext.hasAnnotation
import android.arch.persistence.room.ext.hasAnyOf
@@ -109,6 +110,17 @@
executableElement = it).process()
} ?: emptyList()
+ val transactionMethods = allMembers.filter {
+ it.hasAnnotation(Transaction::class)
+ && it.kind == ElementKind.METHOD
+ // TODO: Exclude abstract methods and let @Query handle that case
+ }.map {
+ TransactionMethodProcessor(
+ baseContext = context,
+ containing = declaredType,
+ executableElement = MoreElements.asExecutable(it)).process()
+ }
+
val constructors = allMembers
.filter { it.kind == ElementKind.CONSTRUCTOR }
.map { MoreElements.asExecutable(it) }
@@ -139,6 +151,7 @@
insertionMethods = insertionMethods,
deletionMethods = deletionMethods,
updateMethods = updateMethods,
+ transactionMethods = transactionMethods,
constructorParamType = constructorParamType)
}
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 01a4c5f..a576aa2 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
@@ -122,6 +122,9 @@
val UPDATE_MISSING_PARAMS = "Method annotated with" +
" @Update but does not have any parameters to update."
+ val TRANSACTION_METHOD_MODIFIERS = "Method annotated with @Transaction must not be " +
+ "private, final, or abstract."
+
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/TransactionMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
new file mode 100644
index 0000000..f861ebe
--- /dev/null
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.processor
+
+import android.arch.persistence.room.ext.hasAnyOf
+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
+
+class TransactionMethodProcessor(baseContext: Context,
+ val containing: DeclaredType,
+ val executableElement: ExecutableElement) {
+
+ 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)
+
+ return TransactionMethod(
+ element = executableElement,
+ name = executableElement.simpleName.toString())
+ }
+}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt
index 227fa49..57d7598 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt
@@ -26,6 +26,7 @@
val insertionMethods : List<InsertionMethod>,
val deletionMethods : List<DeletionMethod>,
val updateMethods : List<UpdateMethod>,
+ val transactionMethods : List<TransactionMethod>,
val constructorParamType : TypeName?) {
// parsed dao might have a suffix if it is used in multiple databases.
private var suffix : String? = null
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/TransactionMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/TransactionMethod.kt
new file mode 100644
index 0000000..c150aee
--- /dev/null
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/TransactionMethod.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.vo
+
+import javax.lang.model.element.ExecutableElement
+
+class TransactionMethod(val element: ExecutableElement, val name: String)
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 a800966..5c7d8a3 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
@@ -30,6 +30,7 @@
import android.arch.persistence.room.vo.InsertionMethod
import android.arch.persistence.room.vo.QueryMethod
import android.arch.persistence.room.vo.ShortcutMethod
+import android.arch.persistence.room.vo.TransactionMethod
import com.google.auto.common.MoreTypes
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.CodeBlock
@@ -46,6 +47,7 @@
import javax.lang.model.element.Modifier.PRIVATE
import javax.lang.model.element.Modifier.PUBLIC
import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeKind
/**
* Creates the implementation for a class annotated with Dao.
@@ -83,7 +85,7 @@
// delete queries that must be rebuild every single time
val oneOffDeleteOrUpdateQueries = groupedDeleteUpdate[true] ?: emptyList()
val shortcutMethods = createInsertionMethods() +
- createDeletionMethods() + createUpdateMethods() +
+ createDeletionMethods() + createUpdateMethods() + createTransactionMethods() +
createPreparedDeleteOrUpdateQueries(preparedDeleteOrUpdateQueries)
builder.apply {
@@ -159,6 +161,63 @@
return methodBuilder.build()
}
+ private fun createTransactionMethods(): List<PreparedStmtQuery> {
+ return dao.transactionMethods.map {
+ PreparedStmtQuery(emptyMap(), createTransactionMethodBody(it))
+ }
+ }
+
+ private fun createTransactionMethodBody(method: TransactionMethod): MethodSpec {
+ val scope = CodeGenScope(this)
+ val methodBuilder = overrideWithoutAnnotations(method.element, declaredDao).apply {
+ addStatement("$N.beginTransaction()", dbField)
+ beginControlFlow("try").apply {
+ val returnsValue = method.element.returnType.kind != TypeKind.VOID
+ val resultVar = if (returnsValue) {
+ scope.getTmpVar("_result")
+ } else {
+ null
+ }
+ addDelegateToSuperStatement(method.element, resultVar)
+ addStatement("$N.setTransactionSuccessful()", dbField)
+ if (returnsValue) {
+ addStatement("return $N", resultVar)
+ }
+ }
+ nextControlFlow("finally").apply {
+ addStatement("$N.endTransaction()", dbField)
+ }
+ endControlFlow()
+ }
+ return methodBuilder.build()
+ }
+
+ private fun MethodSpec.Builder.addDelegateToSuperStatement(element: ExecutableElement,
+ result: String?) {
+ val params: MutableList<Any> = mutableListOf()
+ val format = buildString {
+ if (result != null) {
+ append("$T $L = ")
+ params.add(element.returnType)
+ params.add(result)
+ }
+ append("super.$N(")
+ params.add(element.simpleName)
+ var first = true
+ element.parameters.forEach {
+ if (first) {
+ first = false
+ } else {
+ append(", ")
+ }
+ append(L)
+ params.add(it.simpleName)
+ }
+ append(")")
+ }
+ addStatement(format, *params.toTypedArray())
+ }
+
private fun createConstructor(dbParam: ParameterSpec,
shortcutMethods: List<PreparedStmtQuery>,
callSuper: Boolean): MethodSpec {
diff --git a/room/compiler/src/test/data/daoWriter/input/ComplexDao.java b/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
index ca90163..89859e5 100644
--- a/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
@@ -31,6 +31,11 @@
mDb = db;
}
+ @Transaction
+ public boolean transactionMethod(int i, String s, long l) {
+ return true;
+ }
+
@Query("SELECT name || lastName as fullName, uid as id FROM user where uid = :id")
abstract public List<FullName> fullNames(int id);
diff --git a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
index 87d0c49..dbb54fb 100644
--- a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
@@ -27,6 +27,18 @@
}
@Override
+ public boolean transactionMethod(int i, String s, long l) {
+ __db.beginTransaction();
+ try {
+ boolean _result = super.transactionMethod(i, s, l);
+ __db.setTransactionSuccessful();
+ return _result;
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
public List<ComplexDao.FullName> fullNames(int id) {
final String _sql = "SELECT name || lastName as fullName, uid as id FROM user where uid = ?";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessorTest.kt
new file mode 100644
index 0000000..a07cbe6
--- /dev/null
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessorTest.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.processor
+
+import android.arch.persistence.room.Dao
+import android.arch.persistence.room.Transaction
+import android.arch.persistence.room.testing.TestInvocation
+import android.arch.persistence.room.testing.TestProcessor
+import android.arch.persistence.room.vo.TransactionMethod
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+@RunWith(JUnit4::class)
+class TransactionMethodProcessorTest {
+
+ companion object {
+ const val DAO_PREFIX = """
+ package foo.bar;
+ import android.arch.persistence.room.*;
+ import java.util.*;
+ @Dao
+ abstract class MyClass {
+ """
+ const val DAO_SUFFIX = "}"
+ }
+
+ @Test
+ fun simple() {
+ singleTransactionMethod(
+ """
+ @Transaction
+ public String doInTransaction(int param) { return null; }
+ """) { transaction, _ ->
+ assertThat(transaction.name, `is`("doInTransaction"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun modifier_private() {
+ singleTransactionMethod(
+ """
+ @Transaction
+ private String doInTransaction(int param) { return null; }
+ """) { transaction, _ ->
+ assertThat(transaction.name, `is`("doInTransaction"))
+ }.failsToCompile().withErrorContaining(ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
+ }
+
+ @Test
+ fun modifier_final() {
+ singleTransactionMethod(
+ """
+ @Transaction
+ public final String doInTransaction(int param) { return null; }
+ """) { transaction, _ ->
+ assertThat(transaction.name, `is`("doInTransaction"))
+ }.failsToCompile().withErrorContaining(ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
+ }
+
+ private fun singleTransactionMethod(vararg input: String,
+ handler: (TransactionMethod, TestInvocation) -> Unit):
+ CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
+ TransactionMethodProcessorTest.DAO_PREFIX + input.joinToString("\n") +
+ TransactionMethodProcessorTest.DAO_SUFFIX
+ )))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(Transaction::class, Dao::class)
+ .nextRunHandler { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.java)
+ .map {
+ Pair(it,
+ invocation.processingEnv.elementUtils
+ .getAllMembers(MoreElements.asType(it))
+ .filter {
+ MoreElements.isAnnotationPresent(it,
+ Transaction::class.java)
+ }
+ )
+ }.filter { it.second.isNotEmpty() }.first()
+ val processor = TransactionMethodProcessor(
+ baseContext = invocation.context,
+ containing = MoreTypes.asDeclared(owner.asType()),
+ executableElement = MoreElements.asExecutable(methods.first()))
+ val processed = processor.process()
+ handler(processed, invocation)
+ true
+ }
+ .build())
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
index 337c233..665a1ae 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
@@ -24,6 +24,7 @@
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Transaction;
import android.arch.persistence.room.Update;
import android.arch.persistence.room.integration.testapp.TestDatabase;
import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge;
@@ -259,4 +260,10 @@
+ " WHERE mLastName > :lastName or (mLastName = :lastName and (mName < :name or (mName = :name and mId > :id)))"
+ " ORDER BY mLastName ASC, mName DESC, mId ASC")
public abstract int userComplexCountBefore(String lastName, String name, int id);
+
+ @Transaction
+ public void insertBothByAnnotation(final User a, final User b) {
+ insert(a);
+ insert(b);
+ }
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
index 8861adb..f8049f3 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
@@ -502,4 +502,26 @@
assertThat(mUserDao.updateByAgeAndIds(3f, 30, Arrays.asList(3, 5)), is(1));
assertThat(mUserDao.loadByIds(3)[0].getWeight(), is(3f));
}
+
+ @Test
+ public void transactionByAnnotation() {
+ User a = TestUtil.createUser(3);
+ User b = TestUtil.createUser(5);
+ mUserDao.insertBothByAnnotation(a, b);
+ assertThat(mUserDao.count(), is(2));
+ }
+
+ @Test
+ public void transactionByAnnotation_failure() {
+ User a = TestUtil.createUser(3);
+ User b = TestUtil.createUser(3);
+ boolean caught = false;
+ try {
+ mUserDao.insertBothByAnnotation(a, b);
+ } catch (SQLiteConstraintException e) {
+ caught = true;
+ }
+ assertTrue("SQLiteConstraintException expected", caught);
+ assertThat(mUserDao.count(), is(0));
+ }
}