Support many-to-many with @Relation using @Junction
Expand @Relation to contain a new method, 'associateBy' of type
@Junction that can be used to specify a junction table for resolving
relationships, mainly many-to-many.
Upon using @Junction the query
generated by the RelationCollector is of the form of:
SELECT *, <junction-parent-column>
FROM <junction-table> AS _junction
JOIN <entity-table> ON (<junction-entity-column> = <entity-column>)
WHERE <junction-parent-column> IN (:args)
@Junction supports both an @Entity or @DatabaseView, the Junction table
itself doesn't have to strictly model a many-to-many relation. Both
parentColumn and entityColumn are optional and will be assumed to be
the same as the ones defined in @Relation, otherwise those methods can
be used to specify arbitrary columns in the junction table.
Room will also give a warning for those columns in the junction table
that are not part of a foreign key.
Bug: 63736353
Bug: 69201917
Test: ManyToManyRelationTest, PojoProcessorTest, and others...
Change-Id: I967cf77252391379377ebe0ebfeb23a4fd881ca5
diff --git a/room/common/api/2.2.0-alpha01.ignore b/room/common/api/2.2.0-alpha01.ignore
new file mode 100644
index 0000000..155e4e0
--- /dev/null
+++ b/room/common/api/2.2.0-alpha01.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.room.Relation#associateBy():
+ Added method androidx.room.Relation.associateBy()
diff --git a/room/common/api/2.2.0-alpha01.txt b/room/common/api/2.2.0-alpha01.txt
index d2e3b28..a9643a9 100644
--- a/room/common/api/2.2.0-alpha01.txt
+++ b/room/common/api/2.2.0-alpha01.txt
@@ -120,6 +120,12 @@
method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
}
+ @java.lang.annotation.Target({}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface Junction {
+ method public abstract String entityColumn() default "";
+ method public abstract String parentColumn() default "";
+ method public abstract Class value();
+ }
+
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @IntDef({androidx.room.OnConflictStrategy.REPLACE, androidx.room.OnConflictStrategy.ROLLBACK, androidx.room.OnConflictStrategy.ABORT, androidx.room.OnConflictStrategy.FAIL, androidx.room.OnConflictStrategy.IGNORE}) public @interface OnConflictStrategy {
field public static final int ABORT = 3; // 0x3
field @Deprecated public static final int FAIL = 4; // 0x4
@@ -141,6 +147,7 @@
}
@java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface Relation {
+ method public abstract androidx.room.Junction associateBy() default @androidx.room.Junction;
method public abstract Class entity() default java.lang.Object.class;
method public abstract String entityColumn();
method public abstract String parentColumn();
diff --git a/room/common/api/current.txt b/room/common/api/current.txt
index d2e3b28..a9643a9 100644
--- a/room/common/api/current.txt
+++ b/room/common/api/current.txt
@@ -120,6 +120,12 @@
method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
}
+ @java.lang.annotation.Target({}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface Junction {
+ method public abstract String entityColumn() default "";
+ method public abstract String parentColumn() default "";
+ method public abstract Class value();
+ }
+
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @IntDef({androidx.room.OnConflictStrategy.REPLACE, androidx.room.OnConflictStrategy.ROLLBACK, androidx.room.OnConflictStrategy.ABORT, androidx.room.OnConflictStrategy.FAIL, androidx.room.OnConflictStrategy.IGNORE}) public @interface OnConflictStrategy {
field public static final int ABORT = 3; // 0x3
field @Deprecated public static final int FAIL = 4; // 0x4
@@ -141,6 +147,7 @@
}
@java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface Relation {
+ method public abstract androidx.room.Junction associateBy() default @androidx.room.Junction;
method public abstract Class entity() default java.lang.Object.class;
method public abstract String entityColumn();
method public abstract String parentColumn();
diff --git a/room/common/src/main/java/androidx/room/Junction.java b/room/common/src/main/java/androidx/room/Junction.java
new file mode 100644
index 0000000..6de1661
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Junction.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2019 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 java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Declares a junction to be used for joining a relationship.
+ * <p>
+ * If a {@link Relation} should use an associative table (also know as junction table or join
+ * table) then you can use this annotation to reference such table. This is useful for fetching
+ * many-to-many relations.
+ * <pre>
+ * {@literal @}Entity(primaryKeys = {"pId", "sId"})
+ * public class PlaylistSongXRef {
+ * int pId;
+ * int sId;
+ * }
+ * public class PlaylistWithSongs {
+ * {@literal @}Embedded
+ * Playlist playlist;
+ * {@literal @}Relation(
+ * parentColumn = "playlistId",
+ * entity = Song.class,
+ * entityColumn = "songId",
+ * associateBy = {@literal @}Junction(
+ * value = PlaylistSongXRef.class,
+ * parentColumn = "pId",
+ * entityColumn = "sId)
+ * )
+ * List<String> songs;
+ * }
+ * {@literal @}Dao
+ * public interface MusicDao {
+ * {@literal @}Query("SELECT * FROM Playlist")
+ * List<PlaylistWithSongs> getAllPlaylistsWithSongs();
+ * }
+ * </pre>
+ * <p>
+ * In the above example the many-to-many relationship between {@code Song} and {@code Playlist} has
+ * an associative table defined by the entity {@code PlaylistSongXRef}.
+ *
+ * @see Relation
+ */
+@Target({})
+@Retention(RetentionPolicy.CLASS)
+public @interface Junction {
+ /**
+ * An entity or entity or database view to be used as a junction table when fetching the
+ * relating entities.
+ *
+ * @return The entity or database view to be used as a junction table.
+ */
+ Class value();
+
+ /**
+ * The junction column that will be used to match against the {@link Relation#parentColumn()}.
+ * <p>
+ * If not specified it defaults to {@link Relation#parentColumn()}.
+ */
+ String parentColumn() default "";
+
+ /**
+ * The junction column that will be used to match against the {@link Relation#entityColumn()}.
+ * <p>
+ * If not specified it defaults to {@link Relation#entityColumn()}.
+ */
+ String entityColumn() default "";
+}
diff --git a/room/common/src/main/java/androidx/room/Relation.java b/room/common/src/main/java/androidx/room/Relation.java
index 10221a1..a231b2d 100644
--- a/room/common/src/main/java/androidx/room/Relation.java
+++ b/room/common/src/main/java/androidx/room/Relation.java
@@ -27,24 +27,24 @@
*
* <pre>
* {@literal @}Entity
- * public class Pet {
+ * public class Song {
* {@literal @} PrimaryKey
- * int id;
- * int userId;
+ * int songId;
+ * int albumId;
* String name;
* // other fields
* }
- * public class UserNameAndAllPets {
- * public int id;
- * public String name;
- * {@literal @}Relation(parentColumn = "id", entityColumn = "userId")
- * public List<Pet> pets;
+ * public class AlbumNameAndAllSongs {
+ * int id;
+ * String name;
+ * {@literal @}Relation(parentColumn = "id", entityColumn = "albumId")
+ * List<Song> songs;
* }
*
* {@literal @}Dao
- * public interface UserPetDao {
- * {@literal @}Query("SELECT id, name from User")
- * public List<UserNameAndAllPets> loadUserAndPets();
+ * public interface MusicDao {
+ * {@literal @}Query("SELECT id, name FROM Album")
+ * List<AlbumNameAndAllSongs> loadAlbumAndSongs();
* }
* </pre>
* <p>
@@ -53,51 +53,56 @@
* If you would like to return a different object, you can specify the {@link #entity()} property
* in the annotation.
* <pre>
- * public class User {
+ * public class Album {
* int id;
* // other fields
* }
- * public class PetNameAndId {
- * int id;
+ * public class SongNameAndId {
+ * int songId;
* String name;
* }
- * public class UserAllPets {
- * {@literal @}Embedded
- * public User user;
- * {@literal @}Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class)
- * public List<PetNameAndId> pets;
+ * public class AlbumAllSongs {
+ * {@literal @}Embedded
+ * Album album;
+ * {@literal @}Relation(parentColumn = "id", entityColumn = "albumId", entity = Song.class)
+ * List<SongNameAndId> songs;
* }
* {@literal @}Dao
- * public interface UserPetDao {
- * {@literal @}Query("SELECT * from User")
- * public List<UserAllPets> loadUserAndPets();
+ * public interface MusicDao {
+ * {@literal @}Query("SELECT * from Album")
+ * List<AlbumAllSongs> loadAlbumAndSongs();
* }
* </pre>
* <p>
- * In the example above, {@code PetNameAndId} is a regular Pojo but all of fields are fetched
- * from the {@code entity} defined in the {@code @Relation} annotation (<i>Pet</i>).
- * {@code PetNameAndId} could also define its own relations all of which would also be fetched
+ * In the example above, {@code SongNameAndId} is a regular Pojo but all of fields are fetched
+ * from the {@code entity} defined in the {@code @Relation} annotation (<i>Song</i>).
+ * {@code SongNameAndId} could also define its own relations all of which would also be fetched
* automatically.
* <p>
* If you would like to specify which columns are fetched from the child {@link Entity}, you can
* use {@link #projection()} property in the {@code Relation} annotation.
* <pre>
- * public class UserAndAllPets {
- * {@literal @}Embedded
- * public User user;
- * {@literal @}Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class,
- * projection = {"name"})
- * public List<String> petNames;
+ * public class AlbumAndAllSongs {
+ * {@literal @}Embedded
+ * Album album;
+ * {@literal @}Relation(
+ * parentColumn = "id",
+ * entityColumn = "albumId",
+ * entity = Song.class,
+ * projection = {"name"})
+ * List<String> songNames;
* }
* </pre>
* <p>
+ * If the relationship is defined by an associative table (also know as junction table) then you can
+ * use {@link #associateBy()} to specify it. This is useful for fetching many-to-many relations.
+ * <p>
* Note that {@code @Relation} annotation can be used only in Pojo classes, an {@link Entity} class
* cannot have relations. This is a design decision to avoid common pitfalls in {@link Entity}
* setups. You can read more about it in the main Room documentation. When loading data, you can
* simply work around this limitation by creating Pojo classes that extend the {@link Entity}.
- * <p>
- * Note that the {@code @Relation} annotated field cannot be a constructor parameter, it must be
- * public or have a public setter.
+ *
+ * @see Junction
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
@@ -111,28 +116,38 @@
Class entity() default Object.class;
/**
- * Reference field in the parent Pojo.
+ * Reference column in the parent Pojo.
* <p>
- * If you would like to access to a sub item of a {@link Embedded}d field, you can use
- * the {@code .} notation.
- * <p>
- * For instance, if you have a {@link Embedded}d field named {@code user} with a sub field
- * {@code id}, you can reference it via {@code user.id}.
- * <p>
- * This value will be matched against the value defined in {@link #entityColumn()}.
+ * In a one-to-one or one-to-many relation, this value will be matched against the column
+ * defined in {@link #entityColumn()}. In a many-to-many using {@link #associateBy()} then
+ * this value will be matched against the {@link Junction#parentColumn()}
*
- * @return The field reference in the parent object.
+ * @return The column reference in the parent object.
*/
String parentColumn();
/**
- * The field path to match in the {@link #entity()}. This value will be matched against the
- * value defined in {@link #parentColumn()}.
+ * The column to match in the {@link #entity()}.
+ * <p>
+ * In a one-to-one or one-to-many relation, this value will be matched against the column
+ * defined in {@link #parentColumn()} ()}. In a many-to-many using {@link #associateBy()} then
+ * this value will be matched against the {@link Junction#entityColumn()}
*/
String entityColumn();
/**
- * If sub fields should be fetched from the entity, you can specify them using this field.
+ * The entity or view to be used as a associative table (also known as a junction table) when
+ * fetching the relating entities.
+ *
+ * @return The junction describing the associative table. By default, no junction is specified
+ * and none will be used.
+ *
+ * @see Junction
+ */
+ Junction associateBy() default @Junction(Object.class);
+
+ /**
+ * If sub columns should be fetched from the entity, you can specify them using this field.
* <p>
* By default, inferred from the the return type.
*
diff --git a/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt b/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt
index e6a7edb..3fb705f 100644
--- a/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt
@@ -98,13 +98,14 @@
interface ClassGetter {
fun getAsTypeMirror(methodName: String): TypeMirror?
fun getAsTypeMirrorList(methodName: String): List<TypeMirror>
- fun <T : Annotation> getAsAnnotationBox(methodName: String): Array<AnnotationBox<T>>
+ fun <T : Annotation> getAsAnnotationBox(methodName: String): AnnotationBox<T>
+ fun <T : Annotation> getAsAnnotationBoxArray(methodName: String): Array<AnnotationBox<T>>
}
/**
* Class that helps to read values from annotations. Simple types as string, int, lists can
* be read from [value]. If you need to read classes or another annotations from annotation use
- * [getAsTypeMirror] and [getAsAnnotationBox] correspondingly.
+ * [getAsTypeMirror], [getAsAnnotationBox] and [getAsAnnotationBoxArray] correspondingly.
*/
class AnnotationBox<T : Annotation>(private val obj: Any) : ClassGetter by (obj as ClassGetter) {
@Suppress("UNCHECKED_CAST")
@@ -133,6 +134,10 @@
}
}
returnType == Int::class.java -> value.getAsInt(defaultValue as Int?)
+ returnType.isAnnotation -> {
+ @Suppress("UNCHECKED_CAST")
+ AnnotationClassVisitor(returnType as Class<out Annotation>).visit(value)
+ }
returnType.isArray && returnType.componentType.isAnnotation -> {
@Suppress("UNCHECKED_CAST")
ListVisitor(returnType.componentType as Class<out Annotation>).visit(value)
@@ -151,6 +156,7 @@
ClassGetter::getAsTypeMirror.name -> map[args[0]]
ClassGetter::getAsTypeMirrorList.name -> map[args[0]]
"getAsAnnotationBox" -> map[args[0]]
+ "getAsAnnotationBoxArray" -> map[args[0]]
else -> map[method.name]
}
})
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/EntityProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/EntityProcessor.kt
index 0ae258e..5b7b809 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/EntityProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/EntityProcessor.kt
@@ -41,7 +41,7 @@
}
fun extractIndices(annotation: AnnotationBox<Entity>, tableName: String): List<IndexInput> {
- return annotation.getAsAnnotationBox<androidx.room.Index>("indices").map {
+ return annotation.getAsAnnotationBoxArray<androidx.room.Index>("indices").map {
val indexAnnotation = it.value
val nameValue = indexAnnotation.name
val name = if (nameValue == "") {
@@ -58,7 +58,7 @@
}
fun extractForeignKeys(annotation: AnnotationBox<Entity>): List<ForeignKeyInput> {
- return annotation.getAsAnnotationBox<ForeignKey>("foreignKeys")
+ return annotation.getAsAnnotationBoxArray<ForeignKey>("foreignKeys")
.mapNotNull { annotationBox ->
val foreignKey = annotationBox.value
val parent = annotationBox.getAsTypeMirror("entity")
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
index fdd3941..4da95da3 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
@@ -19,6 +19,7 @@
import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Ignore
+import androidx.room.Junction
import androidx.room.PrimaryKey
import androidx.room.Relation
import androidx.room.ext.extendsBoundOrSelf
@@ -41,6 +42,7 @@
import androidx.room.vo.CallType
import androidx.room.vo.Constructor
import androidx.room.vo.EmbeddedField
+import androidx.room.vo.Entity
import androidx.room.vo.EntityOrView
import androidx.room.vo.Field
import androidx.room.vo.FieldGetter
@@ -491,7 +493,6 @@
// now find the field in the entity.
val entityField = entity.findFieldByColumnName(annotation.value.entityColumn)
-
if (entityField == null) {
context.logger.e(relationElement,
ProcessorErrors.relationCannotFindEntityField(
@@ -501,6 +502,82 @@
return null
}
+ // do we have a join entity?
+ val junctionAnnotation = annotation.getAsAnnotationBox<Junction>("associateBy")
+ val junctionClassInput = junctionAnnotation.getAsTypeMirror("value")
+ val junctionElement: TypeElement? = if (junctionClassInput != null &&
+ !MoreTypes.isTypeOf(Any::class.java, junctionClassInput)) {
+ junctionClassInput.asTypeElement()
+ } else {
+ null
+ }
+ val junction = junctionElement?.let {
+ val entityOrView = EntityOrViewProcessor(context, it, referenceStack).process()
+
+ fun findAndValidateJunctionColumn(
+ columnName: String,
+ onMissingField: () -> Unit
+ ): Field? {
+ val field = entityOrView.findFieldByColumnName(columnName)
+ if (field == null) {
+ onMissingField()
+ return null
+ }
+ if (entityOrView is Entity) {
+ // warn about not having indices in the junction columns, only considering
+ // 1st column in composite primary key and indices, since order matters.
+ val coveredColumns = entityOrView.primaryKey.fields.columnNames.first() +
+ entityOrView.indices.map { it.columnNames.first() }
+ if (!coveredColumns.contains(field.columnName)) {
+ context.logger.w(Warning.MISSING_INDEX_ON_JUNCTION, field.element,
+ ProcessorErrors.junctionColumnWithoutIndex(
+ entityName = entityOrView.typeName.toString(),
+ columnName = columnName))
+ }
+ }
+ return field
+ }
+
+ val junctionParentColumn = if (junctionAnnotation.value.parentColumn.isNotEmpty()) {
+ junctionAnnotation.value.parentColumn
+ } else {
+ parentField.columnName
+ }
+ val junctionParentField = findAndValidateJunctionColumn(
+ columnName = junctionParentColumn,
+ onMissingField = {
+ context.logger.e(junctionElement,
+ ProcessorErrors.relationCannotFindJunctionParentField(
+ entityName = entityOrView.typeName.toString(),
+ columnName = junctionParentColumn,
+ availableColumns = entityOrView.columnNames))
+ })
+
+ val junctionEntityColumn = if (junctionAnnotation.value.entityColumn.isNotEmpty()) {
+ junctionAnnotation.value.entityColumn
+ } else {
+ entityField.columnName
+ }
+ val junctionEntityField = findAndValidateJunctionColumn(
+ columnName = junctionEntityColumn,
+ onMissingField = {
+ context.logger.e(junctionElement,
+ ProcessorErrors.relationCannotFindJunctionEntityField(
+ entityName = entityOrView.typeName.toString(),
+ columnName = junctionEntityColumn,
+ availableColumns = entityOrView.columnNames))
+ })
+
+ if (junctionParentField == null || junctionEntityField == null) {
+ return null
+ }
+
+ androidx.room.vo.Junction(
+ entity = entityOrView,
+ parentField = junctionParentField,
+ entityField = junctionEntityField)
+ }
+
val field = Field(
element = relationElement,
name = relationElement.simpleName.toString(),
@@ -523,6 +600,7 @@
field = field,
parentField = parentField,
entityField = entityField,
+ junction = junction,
projection = projection
)
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index f70ec21..93b3435 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -378,6 +378,30 @@
" Options: ${availableColumns.joinToString(", ")}"
}
+ fun relationCannotFindJunctionEntityField(
+ entityName: String,
+ columnName: String,
+ availableColumns: List<String>
+ ): String {
+ return "Cannot find the child entity referencing column `$columnName` in the junction " +
+ "$entityName. Options: ${availableColumns.joinToString(", ")}"
+ }
+
+ fun relationCannotFindJunctionParentField(
+ entityName: String,
+ columnName: String,
+ availableColumns: List<String>
+ ): String {
+ return "Cannot find the parent entity referencing column `$columnName` in the junction " +
+ "$entityName. Options: ${availableColumns.joinToString(", ")}"
+ }
+
+ fun junctionColumnWithoutIndex(entityName: String, columnName: String) =
+ "The column $columnName in the junction entity $entityName is being used to resolve " +
+ "a relationship but it is not covered by any index. This might cause a " +
+ "full table scan when resolving the relationship, it is highly advised to " +
+ "create an index that covers this column."
+
val RELATION_IN_ENTITY = "Entities cannot have relations."
val CANNOT_FIND_TYPE = "Cannot find type."
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Junction.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Junction.kt
new file mode 100644
index 0000000..d07bc1d
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Junction.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2019 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.vo
+
+/**
+ * Value object defining a junction table for a [Relation].
+ */
+data class Junction(
+ val entity: EntityOrView,
+ val parentField: Field,
+ val entityField: Field
+)
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Relation.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Relation.kt
index 562b376..233a707 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/Relation.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Relation.kt
@@ -33,20 +33,33 @@
// the field referenced for querying. does not need to be in the response but the query
// we generate always has it in the response.
val entityField: Field,
+ // Used for joining on a many-to-many relation
+ val junction: Junction?,
// the projection for the query
val projection: List<String>
) {
-
val pojoTypeName by lazy { pojoType.typeName() }
fun createLoadAllSql(): String {
- val resultFields = projection.toSet() + entityField.columnName
+ val resultFields = projection.toSet()
return createSelect(resultFields)
}
- private fun createSelect(resultFields: Set<String>): String {
- return "SELECT ${resultFields.joinToString(",") {"`$it`"}}" +
- " FROM `${entity.tableName}`" +
- " WHERE `${entityField.columnName}` IN (:args)"
+ private fun createSelect(resultFields: Set<String>) = buildString {
+ if (junction != null) {
+ val resultColumns = resultFields.map { "`${entity.tableName}`.`$it` AS `$it`" } +
+ "_junction.`${junction.parentField.columnName}`"
+ append("SELECT ${resultColumns.joinToString(",")}")
+ append(" FROM `${junction.entity.tableName}` AS _junction")
+ append(" INNER JOIN `${entity.tableName}` ON" +
+ " (_junction.`${junction.entityField.columnName}`" +
+ " = `${entity.tableName}`.`${entityField.columnName}`)")
+ append(" WHERE _junction.`${junction.parentField.columnName}` IN (:args)")
+ } else {
+ val resultColumns = resultFields.map { "`$it`" }.toSet() + "`${entityField.columnName}`"
+ append("SELECT ${resultColumns.joinToString(",")}")
+ append(" FROM `${entity.tableName}`")
+ append(" WHERE `${entityField.columnName}` IN (:args)")
+ }
}
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt
index c92ce97..0abd15e9 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt
@@ -35,6 +35,7 @@
RELATION_QUERY_WITHOUT_TRANSACTION("ROOM_RELATION_QUERY_WITHOUT_TRANSACTION"),
DEFAULT_CONSTRUCTOR("ROOM_DEFAULT_CONSTRUCTOR"),
MISSING_COPY_ANNOTATIONS("MISSING_COPY_ANNOTATIONS"),
+ MISSING_INDEX_ON_JUNCTION("MISSING_INDEX_ON_JUNCTION"),
JDK_VERSION_HAS_BUG("JDK_VERSION_HAS_BUG");
companion object {
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt
index b4860c4..be346d1 100644
--- a/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt
@@ -142,9 +142,20 @@
if (shouldCopyCursor) "true" else "false")
beginControlFlow("try").apply {
- addStatement("final $T $L = $T.getColumnIndex($L, $S)",
- TypeName.INT, itemKeyIndexVar, RoomTypeNames.CURSOR_UTIL, cursorVar,
- relation.entityField.columnName)
+ if (relation.junction != null) {
+ // when using a junction table the relationship map is keyed on the parent
+ // reference column of the junction table, the same column used in the WHERE IN
+ // clause, this column is the rightmost column in the generated SELECT
+ // clause.
+ val junctionParentColumnIndex = relation.projection.size
+ addStatement("final $T $L = $L; // $L",
+ TypeName.INT, itemKeyIndexVar, junctionParentColumnIndex,
+ relation.junction.parentField.columnName)
+ } else {
+ addStatement("final $T $L = $T.getColumnIndex($L, $S)",
+ TypeName.INT, itemKeyIndexVar, RoomTypeNames.CURSOR_UTIL, cursorVar,
+ relation.entityField.columnName)
+ }
beginControlFlow("if ($L == -1)", itemKeyIndexVar).apply {
addStatement("return")
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
index 4c860dc..f6e8e50 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
@@ -23,7 +23,10 @@
import androidx.room.processor.ProcessorErrors.CANNOT_FIND_TYPE
import androidx.room.processor.ProcessorErrors.POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME
import androidx.room.processor.ProcessorErrors.RELATION_NOT_COLLECTION
+import androidx.room.processor.ProcessorErrors.junctionColumnWithoutIndex
import androidx.room.processor.ProcessorErrors.relationCannotFindEntityField
+import androidx.room.processor.ProcessorErrors.relationCannotFindJunctionEntityField
+import androidx.room.processor.ProcessorErrors.relationCannotFindJunctionParentField
import androidx.room.processor.ProcessorErrors.relationCannotFindParentEntityField
import androidx.room.testing.TestInvocation
import androidx.room.vo.CallType
@@ -577,6 +580,266 @@
}
@Test
+ fun relation_associateBy() {
+ val junctionEntity = """
+ package foo.bar;
+
+ import androidx.room.*;
+
+ @Entity(
+ primaryKeys = {"uid","friendId"},
+ foreignKeys = {
+ @ForeignKey(
+ entity = User.class,
+ parentColumns = "uid",
+ childColumns = "uid",
+ onDelete = ForeignKey.CASCADE),
+ @ForeignKey(
+ entity = User.class,
+ parentColumns = "uid",
+ childColumns = "friendId",
+ onDelete = ForeignKey.CASCADE),
+ },
+ indices = { @Index("uid"), @Index("friendId") }
+ )
+ public class UserFriendsXRef {
+ public int uid;
+ public int friendId;
+ }
+ """.toJFO("foo.bar.UserFriendsXRef")
+ singleRun(
+ """
+ int id;
+ @Relation(
+ parentColumn = "id", entityColumn = "uid",
+ associateBy = @Junction(
+ value = UserFriendsXRef.class,
+ parentColumn = "uid", entityColumn = "friendId")
+ )
+ public List<User> user;
+ """, COMMON.USER, junctionEntity
+ ) { pojo ->
+ assertThat(pojo.relations.size, `is`(1))
+ assertThat(pojo.relations.first().junction, notNullValue())
+ assertThat(pojo.relations.first().junction!!.parentField.columnName,
+ `is`("uid"))
+ assertThat(pojo.relations.first().junction!!.entityField.columnName,
+ `is`("friendId"))
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun relation_associateBy_withView() {
+ val junctionEntity = """
+ package foo.bar;
+
+ import androidx.room.*;
+
+ @DatabaseView("SELECT 1, 2, FROM User")
+ public class UserFriendsXRefView {
+ public int uid;
+ public int friendId;
+ }
+ """.toJFO("foo.bar.UserFriendsXRefView")
+ singleRun(
+ """
+ int id;
+ @Relation(
+ parentColumn = "id", entityColumn = "uid",
+ associateBy = @Junction(
+ value = UserFriendsXRefView.class,
+ parentColumn = "uid", entityColumn = "friendId")
+ )
+ public List<User> user;
+ """, COMMON.USER, junctionEntity
+ ) { _ ->
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun relation_associateBy_defaultColumns() {
+ val junctionEntity = """
+ package foo.bar;
+
+ import androidx.room.*;
+
+ @Entity(
+ primaryKeys = {"uid","friendId"},
+ foreignKeys = {
+ @ForeignKey(
+ entity = User.class,
+ parentColumns = "uid",
+ childColumns = "uid",
+ onDelete = ForeignKey.CASCADE),
+ @ForeignKey(
+ entity = User.class,
+ parentColumns = "uid",
+ childColumns = "friendId",
+ onDelete = ForeignKey.CASCADE),
+ },
+ indices = { @Index("uid"), @Index("friendId") }
+ )
+ public class UserFriendsXRef {
+ public int uid;
+ public int friendId;
+ }
+ """.toJFO("foo.bar.UserFriendsXRef")
+ singleRun(
+ """
+ int friendId;
+ @Relation(
+ parentColumn = "friendId", entityColumn = "uid",
+ associateBy = @Junction(UserFriendsXRef.class))
+ public List<User> user;
+ """, COMMON.USER, junctionEntity
+ ) { _ ->
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun relation_associateBy_missingParentColumn() {
+ val junctionEntity = """
+ package foo.bar;
+
+ import androidx.room.*;
+
+ @Entity(primaryKeys = {"friendFrom","uid"})
+ public class UserFriendsXRef {
+ public int friendFrom;
+ public int uid;
+ }
+ """.toJFO("foo.bar.UserFriendsXRef")
+ singleRun(
+ """
+ int id;
+ @Relation(
+ parentColumn = "id", entityColumn = "uid",
+ associateBy = @Junction(UserFriendsXRef.class)
+ )
+ public List<User> user;
+ """, COMMON.USER, junctionEntity
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(relationCannotFindJunctionParentField(
+ "foo.bar.UserFriendsXRef", "id", listOf("friendFrom", "uid")))
+ }
+
+ @Test
+ fun relation_associateBy_missingEntityColumn() {
+ val junctionEntity = """
+ package foo.bar;
+
+ import androidx.room.*;
+
+ @Entity(primaryKeys = {"friendA","friendB"})
+ public class UserFriendsXRef {
+ public int friendA;
+ public int friendB;
+ }
+ """.toJFO("foo.bar.UserFriendsXRef")
+ singleRun(
+ """
+ int friendA;
+ @Relation(
+ parentColumn = "friendA", entityColumn = "uid",
+ associateBy = @Junction(UserFriendsXRef.class)
+ )
+ public List<User> user;
+ """, COMMON.USER, junctionEntity
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(relationCannotFindJunctionEntityField(
+ "foo.bar.UserFriendsXRef", "uid", listOf("friendA", "friendB"))
+ )
+ }
+
+ @Test
+ fun relation_associateBy_missingSpecifiedParentColumn() {
+ val junctionEntity = """
+ package foo.bar;
+
+ import androidx.room.*;
+
+ @Entity(primaryKeys = {"friendA","friendB"})
+ public class UserFriendsXRef {
+ public int friendA;
+ public int friendB;
+ }
+ """.toJFO("foo.bar.UserFriendsXRef")
+ singleRun(
+ """
+ int friendA;
+ @Relation(
+ parentColumn = "friendA", entityColumn = "uid",
+ associateBy = @Junction(
+ value = UserFriendsXRef.class,
+ parentColumn = "bad_col")
+ )
+ public List<User> user;
+ """, COMMON.USER, junctionEntity
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(relationCannotFindJunctionParentField(
+ "foo.bar.UserFriendsXRef", "bad_col", listOf("friendA", "friendB"))
+ )
+ }
+
+ @Test
+ fun relation_associateBy_missingSpecifiedEntityColumn() {
+ val junctionEntity = """
+ package foo.bar;
+
+ import androidx.room.*;
+
+ @Entity(primaryKeys = {"friendA","friendB"})
+ public class UserFriendsXRef {
+ public int friendA;
+ public int friendB;
+ }
+ """.toJFO("foo.bar.UserFriendsXRef")
+ singleRun(
+ """
+ int friendA;
+ @Relation(
+ parentColumn = "friendA", entityColumn = "uid",
+ associateBy = @Junction(
+ value = UserFriendsXRef.class,
+ entityColumn = "bad_col")
+ )
+ public List<User> user;
+ """, COMMON.USER, junctionEntity
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(relationCannotFindJunctionEntityField(
+ "foo.bar.UserFriendsXRef", "bad_col", listOf("friendA", "friendB"))
+ )
+ }
+
+ @Test
+ fun relation_associateBy_warnIndexOnJunctionColumn() {
+ val junctionEntity = """
+ package foo.bar;
+
+ import androidx.room.*;
+
+ @Entity
+ public class UserFriendsXRef {
+ @PrimaryKey(autoGenerate = true)
+ public long rowid;
+ public int uid;
+ public int friendId;
+ }
+ """.toJFO("foo.bar.UserFriendsXRef")
+ singleRun(
+ """
+ int friendId;
+ @Relation(
+ parentColumn = "friendId", entityColumn = "uid",
+ associateBy = @Junction(UserFriendsXRef.class))
+ public List<User> user;
+ """, COMMON.USER, junctionEntity
+ ) { _ ->
+ }.compilesWithoutError().withWarningCount(2).withWarningContaining(
+ junctionColumnWithoutIndex("foo.bar.UserFriendsXRef", "uid"))
+ }
+
+ @Test
fun cache() {
val pojo = """
$HEADER
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/MusicTestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/MusicTestDatabase.java
new file mode 100644
index 0000000..22917de
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/MusicTestDatabase.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 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.integration.testapp;
+
+import androidx.room.Database;
+import androidx.room.RoomDatabase;
+import androidx.room.integration.testapp.dao.MusicDao;
+import androidx.room.integration.testapp.vo.Playlist;
+import androidx.room.integration.testapp.vo.PlaylistMultiSongXRefView;
+import androidx.room.integration.testapp.vo.PlaylistSongXRef;
+import androidx.room.integration.testapp.vo.Song;
+
+@Database(
+ entities = {Song.class, Playlist.class, PlaylistSongXRef.class},
+ views = {PlaylistMultiSongXRefView.class},
+ version = 1,
+ exportSchema = false)
+public abstract class MusicTestDatabase extends RoomDatabase {
+ public abstract MusicDao getDao();
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/TestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/TestDatabase.java
index 3dace5b..a6c6aba 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/TestDatabase.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/TestDatabase.java
@@ -35,6 +35,7 @@
import androidx.room.integration.testapp.dao.WithClauseDao;
import androidx.room.integration.testapp.vo.BlobEntity;
import androidx.room.integration.testapp.vo.Day;
+import androidx.room.integration.testapp.vo.FriendsJunction;
import androidx.room.integration.testapp.vo.FunnyNamedEntity;
import androidx.room.integration.testapp.vo.House;
import androidx.room.integration.testapp.vo.Pet;
@@ -50,7 +51,8 @@
import java.util.Set;
@Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class,
- BlobEntity.class, Product.class, FunnyNamedEntity.class, House.class},
+ BlobEntity.class, Product.class, FunnyNamedEntity.class, House.class,
+ FriendsJunction.class},
views = {PetWithUser.class},
version = 1, exportSchema = false)
@TypeConverters(TestDatabase.Converters.class)
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
new file mode 100644
index 0000000..957fbda
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2019 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.integration.testapp.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.Query;
+import androidx.room.Transaction;
+import androidx.room.integration.testapp.vo.MultiSongPlaylistWithSongs;
+import androidx.room.integration.testapp.vo.Playlist;
+import androidx.room.integration.testapp.vo.PlaylistSongXRef;
+import androidx.room.integration.testapp.vo.PlaylistWithSongTitles;
+import androidx.room.integration.testapp.vo.PlaylistWithSongs;
+import androidx.room.integration.testapp.vo.Song;
+
+import java.util.List;
+
+@Dao
+public interface MusicDao {
+
+ @Insert
+ void addSongs(Song... songs);
+
+ @Insert
+ void addPlaylists(Playlist... playlists);
+
+ @Insert
+ void addPlaylistSongRelation(PlaylistSongXRef... relations);
+
+ @Transaction
+ @Query("SELECT * FROM Playlist")
+ List<PlaylistWithSongs> getAllPlaylistWithSongs();
+
+ @Transaction
+ @Query("SELECT * FROM Playlist WHERE mPlaylistId = :id")
+ LiveData<PlaylistWithSongs> getPlaylistsWithSongsLiveData(int id);
+
+ @Transaction
+ @Query("SELECT * FROM Playlist WHERE mPlaylistId = :playlistId")
+ PlaylistWithSongTitles getPlaylistWithSongTitles(int playlistId);
+
+ @Transaction
+ @Query("SELECT * FROM Playlist")
+ List<MultiSongPlaylistWithSongs> getAllMultiSongPlaylistWithSongs();
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
index 7422e71..1827eba 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
@@ -33,6 +33,7 @@
import androidx.room.integration.testapp.vo.Day;
import androidx.room.integration.testapp.vo.NameAndLastName;
import androidx.room.integration.testapp.vo.User;
+import androidx.room.integration.testapp.vo.UserAndFriends;
import androidx.room.integration.testapp.vo.UserSummary;
import androidx.sqlite.db.SupportSQLiteQuery;
@@ -84,6 +85,9 @@
@Query("select * from user where custommm = :customField")
public abstract List<User> findByCustomField(String customField);
+ @Query("select * from user")
+ public abstract List<UserAndFriends> loadUserAndFriends();
+
@Insert
public abstract void insert(User user);
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserPetDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserPetDao.java
index f3b5ef3..14c3cc1 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserPetDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserPetDao.java
@@ -29,6 +29,7 @@
import androidx.room.integration.testapp.vo.Pet;
import androidx.room.integration.testapp.vo.User;
import androidx.room.integration.testapp.vo.UserAndAllPets;
+import androidx.room.integration.testapp.vo.UserAndAllPetsViaJunction;
import androidx.room.integration.testapp.vo.UserAndGenericPet;
import androidx.room.integration.testapp.vo.UserAndPet;
import androidx.room.integration.testapp.vo.UserAndPetAdoptionDates;
@@ -68,6 +69,10 @@
@Query("SELECT * FROM User u")
List<UserAndAllPets> loadAllUsersWithTheirPets();
+ @Transaction
+ @Query("SELECT * FROM User u")
+ List<UserAndAllPetsViaJunction> loadAllUsersWithTheirPetsViaJunction();
+
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@Transaction
@Query("SELECT * FROM User u")
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java
index 511f292..2978b70 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java
@@ -32,13 +32,18 @@
import androidx.room.InvalidationTrackerTrojan;
import androidx.room.Room;
import androidx.room.integration.testapp.FtsTestDatabase;
+import androidx.room.integration.testapp.MusicTestDatabase;
import androidx.room.integration.testapp.dao.MailDao;
+import androidx.room.integration.testapp.dao.MusicDao;
import androidx.room.integration.testapp.dao.SongDao;
import androidx.room.integration.testapp.vo.AvgWeightByAge;
import androidx.room.integration.testapp.vo.Mail;
import androidx.room.integration.testapp.vo.Pet;
import androidx.room.integration.testapp.vo.PetWithUser;
import androidx.room.integration.testapp.vo.PetsToys;
+import androidx.room.integration.testapp.vo.Playlist;
+import androidx.room.integration.testapp.vo.PlaylistSongXRef;
+import androidx.room.integration.testapp.vo.PlaylistWithSongs;
import androidx.room.integration.testapp.vo.Song;
import androidx.room.integration.testapp.vo.SongDescription;
import androidx.room.integration.testapp.vo.Toy;
@@ -267,6 +272,56 @@
}
@Test
+ public void withRelationAndJunction() throws ExecutionException, InterruptedException,
+ TimeoutException {
+ Context context = ApplicationProvider.getApplicationContext();
+ final MusicTestDatabase db = Room.inMemoryDatabaseBuilder(context, MusicTestDatabase.class)
+ .build();
+ final MusicDao musicDao = db.getDao();
+
+ final Song mSong1 = new Song(
+ 1,
+ "I Know Places",
+ "Taylor Swift",
+ "1989",
+ 195,
+ 2014);
+ final Song mSong2 = new Song(
+ 2,
+ "Blank Space",
+ "Taylor Swift",
+ "1989",
+ 241,
+ 2014);
+
+ final Playlist mPlaylist1 = new Playlist(1);
+ final Playlist mPlaylist2 = new Playlist(2);
+
+ musicDao.addSongs(mSong1, mSong2);
+ musicDao.addPlaylists(mPlaylist1, mPlaylist2);
+
+ musicDao.addPlaylistSongRelation(new PlaylistSongXRef(1, 1));
+
+ LiveData<PlaylistWithSongs> liveData = musicDao.getPlaylistsWithSongsLiveData(1);
+
+ final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ final TestObserver<PlaylistWithSongs> observer = new MyTestObserver<>();
+ TestUtil.observeOnMainThread(liveData, lifecycleOwner, observer);
+
+ assertThat(observer.get().songs.size(), is(1));
+ assertThat(observer.get().songs.get(0), is(mSong1));
+
+ observer.reset();
+
+ musicDao.addPlaylistSongRelation(new PlaylistSongXRef(1, 2));
+
+ assertThat(observer.get().songs.size(), is(2));
+ assertThat(observer.get().songs.get(0), is(mSong1));
+ assertThat(observer.get().songs.get(1), is(mSong2));
+ }
+
+ @Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
public void withWithClause() throws ExecutionException, InterruptedException,
TimeoutException {
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ManyToManyRelationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ManyToManyRelationTest.java
new file mode 100644
index 0000000..169a1f9
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ManyToManyRelationTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2019 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.integration.testapp.test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import android.content.Context;
+
+import androidx.room.Room;
+import androidx.room.integration.testapp.MusicTestDatabase;
+import androidx.room.integration.testapp.dao.MusicDao;
+import androidx.room.integration.testapp.vo.MultiSongPlaylistWithSongs;
+import androidx.room.integration.testapp.vo.Playlist;
+import androidx.room.integration.testapp.vo.PlaylistSongXRef;
+import androidx.room.integration.testapp.vo.PlaylistWithSongTitles;
+import androidx.room.integration.testapp.vo.PlaylistWithSongs;
+import androidx.room.integration.testapp.vo.Song;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ManyToManyRelationTest {
+
+ private MusicDao mMusicDao;
+
+ private final Song mSong1 = new Song(
+ 1,
+ "Desde el Corazón",
+ "Bad Bunny",
+ "BPPR Single",
+ 127,
+ 2019);
+ private final Song mSong2 = new Song(
+ 2,
+ "Cambumbo",
+ "Tego Calderon",
+ "El Abayarde",
+ 180,
+ 2002);
+
+ private final Playlist mPlaylist1 = new Playlist(1);
+ private final Playlist mPlaylist2 = new Playlist(2);
+
+ @Before
+ public void createDb() {
+ Context context = ApplicationProvider.getApplicationContext();
+ MusicTestDatabase db = Room.inMemoryDatabaseBuilder(context, MusicTestDatabase.class)
+ .build();
+ mMusicDao = db.getDao();
+ }
+
+ @Test
+ public void relationWithJumpTable() {
+ mMusicDao.addSongs(mSong1, mSong2);
+ mMusicDao.addPlaylists(mPlaylist1, mPlaylist2);
+
+ mMusicDao.addPlaylistSongRelation(
+ new PlaylistSongXRef(1, 1),
+ new PlaylistSongXRef(1, 2),
+ new PlaylistSongXRef(2, 1)
+ );
+
+ List<PlaylistWithSongs> playlistWithSongs = mMusicDao.getAllPlaylistWithSongs();
+ assertThat(playlistWithSongs.size(), is(2));
+
+ assertThat(playlistWithSongs.get(0).playlist, is(mPlaylist1));
+ assertThat(playlistWithSongs.get(0).songs.size(), is(2));
+ assertThat(playlistWithSongs.get(0).songs.get(0), is(mSong1));
+ assertThat(playlistWithSongs.get(0).songs.get(1), is(mSong2));
+
+ assertThat(playlistWithSongs.get(1).playlist, is(mPlaylist2));
+ assertThat(playlistWithSongs.get(1).songs.size(), is(1));
+ assertThat(playlistWithSongs.get(1).songs.get(0), is(mSong1));
+ }
+
+ @Test
+ public void relationWithJumpTable_projection() {
+ mMusicDao.addSongs(mSong1, mSong2);
+ mMusicDao.addPlaylists(mPlaylist1, mPlaylist2);
+
+ mMusicDao.addPlaylistSongRelation(
+ new PlaylistSongXRef(1, 1),
+ new PlaylistSongXRef(1, 2),
+ new PlaylistSongXRef(2, 1)
+ );
+
+ PlaylistWithSongTitles playlistWithSongTitles = mMusicDao.getPlaylistWithSongTitles(1);
+
+ assertThat(playlistWithSongTitles.playlist, is(mPlaylist1));
+ assertThat(playlistWithSongTitles.titles.size(), is(2));
+ assertThat(playlistWithSongTitles.titles.get(0), is(mSong1.mTitle));
+ assertThat(playlistWithSongTitles.titles.get(1), is(mSong2.mTitle));
+ }
+
+ @Test
+ public void relationWithJumpView() {
+ mMusicDao.addSongs(mSong1, mSong2);
+ mMusicDao.addPlaylists(mPlaylist1, mPlaylist2);
+
+ mMusicDao.addPlaylistSongRelation(
+ new PlaylistSongXRef(1, 1),
+ new PlaylistSongXRef(1, 2),
+ new PlaylistSongXRef(2, 1)
+ );
+
+ List<MultiSongPlaylistWithSongs> playlistWithSongs =
+ mMusicDao.getAllMultiSongPlaylistWithSongs();
+ assertThat(playlistWithSongs.size(), is(2));
+
+ assertThat(playlistWithSongs.get(0).playlist, is(mPlaylist1));
+ assertThat(playlistWithSongs.get(0).songs.size(), is(2));
+ assertThat(playlistWithSongs.get(0).songs.get(0), is(mSong1));
+ assertThat(playlistWithSongs.get(0).songs.get(1), is(mSong2));
+
+ assertThat(playlistWithSongs.get(1).playlist, is(mPlaylist2));
+ assertThat(playlistWithSongs.get(1).songs.size(), is(0));
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoWithRelationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoWithRelationTest.java
index fd303aa..13fccd8 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoWithRelationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoWithRelationTest.java
@@ -27,6 +27,7 @@
import androidx.room.integration.testapp.vo.Toy;
import androidx.room.integration.testapp.vo.User;
import androidx.room.integration.testapp.vo.UserAndAllPets;
+import androidx.room.integration.testapp.vo.UserAndAllPetsViaJunction;
import androidx.room.integration.testapp.vo.UserAndPetAdoptionDates;
import androidx.room.integration.testapp.vo.UserAndPetsAndHouses;
import androidx.room.integration.testapp.vo.UserIdAndPetNames;
@@ -261,4 +262,25 @@
assertThat(result.get(i).getHouses(), is(houses.get(i)));
}
}
+
+ @Test
+ public void viaJunction() {
+ User[] users = TestUtil.createUsersArray(1, 2, 3);
+ Pet[][] userPets = new Pet[3][];
+ mUserDao.insertAll(users);
+ for (User user : users) {
+ Pet[] pets = TestUtil.createPetsForUser(user.getId(), user.getId() * 10,
+ user.getId() - 1);
+ mPetDao.insertAll(pets);
+ userPets[user.getId() - 1] = pets;
+ }
+ List<UserAndAllPets> usersAndPets = mUserPetDao.loadAllUsersWithTheirPets();
+ List<UserAndAllPetsViaJunction> userAndPetsViaJunctions =
+ mUserPetDao.loadAllUsersWithTheirPetsViaJunction();
+ assertThat(usersAndPets.size(), is(userAndPetsViaJunctions.size()));
+ for (int i = 0; i < usersAndPets.size(); i++) {
+ assertThat(usersAndPets.get(i).user, is(userAndPetsViaJunctions.get(i).user));
+ assertThat(usersAndPets.get(i).pets, is(userAndPetsViaJunctions.get(i).pets));
+ }
+ }
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/FriendsJunction.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/FriendsJunction.java
new file mode 100644
index 0000000..41e47b1
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/FriendsJunction.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import static androidx.room.ForeignKey.CASCADE;
+
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+
+@Entity(
+ primaryKeys = {"friendA", "friendB"},
+ foreignKeys = {
+ @ForeignKey(
+ entity = User.class,
+ parentColumns = "mId",
+ childColumns = "friendA",
+ onDelete = CASCADE),
+ @ForeignKey(
+ entity = User.class,
+ parentColumns = "mId",
+ childColumns = "friendB",
+ onDelete = CASCADE),
+ })
+public class FriendsJunction {
+ public final int friendA;
+ public final int friendB;
+
+ public FriendsJunction(int friendA, int friendB) {
+ this.friendA = friendA;
+ this.friendB = friendB;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/MultiSongPlaylistWithSongs.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/MultiSongPlaylistWithSongs.java
new file mode 100644
index 0000000..91853bb
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/MultiSongPlaylistWithSongs.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Junction;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class MultiSongPlaylistWithSongs {
+ @Embedded
+ public Playlist playlist;
+ @Relation(
+ parentColumn = "mPlaylistId",
+ entity = Song.class,
+ entityColumn = "mSongId",
+ associateBy = @Junction(PlaylistMultiSongXRefView.class))
+ public List<Song> songs;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Playlist.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Playlist.java
new file mode 100644
index 0000000..7c0fe62
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Playlist.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@Entity
+public class Playlist {
+ @PrimaryKey
+ public final int mPlaylistId;
+
+ public Playlist(int playlistId) {
+ mPlaylistId = playlistId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Playlist playlist = (Playlist) o;
+
+ return mPlaylistId == playlist.mPlaylistId;
+ }
+
+ @Override
+ public int hashCode() {
+ return mPlaylistId;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistMultiSongXRefView.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistMultiSongXRefView.java
new file mode 100644
index 0000000..067ca320
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistMultiSongXRefView.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.DatabaseView;
+
+// View of join table with playlists with more than 1 song
+@DatabaseView("SELECT * FROM PlaylistSongXRef WHERE mPlaylistId IN (SELECT mPlaylistId FROM"
+ + " PlaylistSongXRef GROUP BY mPlaylistId HAVING COUNT(mSongId) > 1)")
+public class PlaylistMultiSongXRefView {
+ public final int mPlaylistId;
+ public final int mSongId;
+
+ public PlaylistMultiSongXRefView(int playlistId, int songId) {
+ mPlaylistId = playlistId;
+ mSongId = songId;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistSongXRef.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistSongXRef.java
new file mode 100644
index 0000000..9f0e768
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistSongXRef.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import static androidx.room.ForeignKey.CASCADE;
+
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+
+@Entity(
+ primaryKeys = {"mPlaylistId", "mSongId"},
+ foreignKeys = {
+ @ForeignKey(
+ entity = Playlist.class,
+ parentColumns = "mPlaylistId",
+ childColumns = "mPlaylistId",
+ onDelete = CASCADE),
+ @ForeignKey(
+ entity = Song.class,
+ parentColumns = "mSongId",
+ childColumns = "mSongId",
+ onDelete = CASCADE),
+ }
+)
+public class PlaylistSongXRef {
+ public final int mPlaylistId;
+ public final int mSongId;
+
+ public PlaylistSongXRef(int playlistId, int songId) {
+ mPlaylistId = playlistId;
+ mSongId = songId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PlaylistSongXRef that = (PlaylistSongXRef) o;
+
+ if (mPlaylistId != that.mPlaylistId) return false;
+ return mSongId == that.mSongId;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mPlaylistId;
+ result = 31 * result + mSongId;
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistWithSongTitles.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistWithSongTitles.java
new file mode 100644
index 0000000..799c9e7b
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistWithSongTitles.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Junction;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class PlaylistWithSongTitles {
+ @Embedded
+ public Playlist playlist;
+ @Relation(
+ parentColumn = "mPlaylistId",
+ entity = Song.class,
+ entityColumn = "mSongId",
+ associateBy = @Junction(PlaylistSongXRef.class),
+ projection = "mTitle")
+ public List<String> titles;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistWithSongs.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistWithSongs.java
new file mode 100644
index 0000000..975cfc18
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistWithSongs.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Junction;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class PlaylistWithSongs {
+ @Embedded
+ public Playlist playlist;
+ @Relation(
+ parentColumn = "mPlaylistId",
+ entity = Song.class,
+ entityColumn = "mSongId",
+ associateBy = @Junction(PlaylistSongXRef.class))
+ public List<Song> songs;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndAllPetsViaJunction.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndAllPetsViaJunction.java
new file mode 100644
index 0000000..c9b244a
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndAllPetsViaJunction.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Junction;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class UserAndAllPetsViaJunction {
+ @Embedded
+ public User user;
+ @Relation(
+ parentColumn = "mId", entityColumn = "mPetId",
+ associateBy = @Junction(
+ value = Pet.class,
+ parentColumn = "mUserId",
+ entityColumn = "mPetId"
+ )
+ )
+ public List<Pet> pets;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndFriends.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndFriends.java
new file mode 100644
index 0000000..93b20c9
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndFriends.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Junction;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class UserAndFriends {
+ @Embedded
+ public final User user;
+
+ @Relation(
+ entity = User.class,
+ parentColumn = "mId",
+ entityColumn = "mId",
+ associateBy = @Junction(
+ value = FriendsJunction.class,
+ parentColumn = "friendA",
+ entityColumn = "friendB")
+ )
+ public final List<User> friends;
+
+ public UserAndFriends(User user, List<User> friends) {
+ this.user = user;
+ this.friends = friends;
+ }
+}