Supporting multimap return type for JOIN statements in Room.
This CL mainly focuses on supporting the multimaps of type Map<Key, List<Value>>. In the follow-up CLs we will be supporting Map<Key, Set<Value>> as well as 1-1 mappings.
Bug: 187490856
Test: JoinQueryTest.java
Change-Id: I27c0654cc2eb167d59a054c0ac0c1a5408b627b4
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
index 22917de..f9b4e1a 100644
--- 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
@@ -19,13 +19,15 @@
import androidx.room.Database;
import androidx.room.RoomDatabase;
import androidx.room.integration.testapp.dao.MusicDao;
+import androidx.room.integration.testapp.vo.Album;
+import androidx.room.integration.testapp.vo.Artist;
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},
+ entities = {Song.class, Playlist.class, PlaylistSongXRef.class, Artist.class, Album.class},
views = {PlaylistMultiSongXRefView.class},
version = 1,
exportSchema = false)
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
index 957fbda..604ec0e 100644
--- 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
@@ -20,23 +20,42 @@
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
+import androidx.room.RawQuery;
+import androidx.room.RoomWarnings;
import androidx.room.Transaction;
+import androidx.room.integration.testapp.vo.Album;
+import androidx.room.integration.testapp.vo.AlbumNameAndBandName;
+import androidx.room.integration.testapp.vo.AlbumWithSongs;
+import androidx.room.integration.testapp.vo.Artist;
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.ReleasedAlbum;
import androidx.room.integration.testapp.vo.Song;
+import androidx.sqlite.db.SupportSQLiteQuery;
import java.util.List;
+import java.util.Map;
+
+import io.reactivex.Flowable;
@Dao
+@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
+// TODO: (b/191693863) Cannot use @RewriteQueriesToDropUnusedColumns due to this bug.
public interface MusicDao {
@Insert
void addSongs(Song... songs);
@Insert
+ void addArtists(Artist... artists);
+
+ @Insert
+ void addAlbums(Album... albums);
+
+ @Insert
void addPlaylists(Playlist... playlists);
@Insert
@@ -57,4 +76,26 @@
@Transaction
@Query("SELECT * FROM Playlist")
List<MultiSongPlaylistWithSongs> getAllMultiSongPlaylistWithSongs();
+
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
+ Map<Artist, List<Song>> getAllArtistAndTheirSongs();
+
+ @Query("SELECT * FROM Artist JOIN Album ON Artist.mArtistName = Album.mAlbumArtist")
+ Map<Artist, List<AlbumWithSongs>> getAllArtistAndTheirAlbumsWithSongs();
+
+ @RawQuery
+ Map<Artist, List<Song>> getAllArtistAndTheirSongsRawQuery(SupportSQLiteQuery query);
+
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
+ LiveData<Map<Artist, List<Song>>> getAllArtistAndTheirSongsAsLiveData();
+
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
+ Flowable<Map<Artist, List<Song>>> getAllArtistAndTheirSongsAsFlowable();
+
+ @Query("SELECT Album.mAlbumReleaseYear as mReleaseYear, Album.mAlbumName, Album.mAlbumArtist "
+ + "as mBandName"
+ + " from Album "
+ + "JOIN Song "
+ + "ON Album.mAlbumArtist = Song.mArtist AND Album.mAlbumName = Song.mAlbum")
+ Map<ReleasedAlbum, List<AlbumNameAndBandName>> getReleaseYearToAlbumsAndBands();
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultimapQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultimapQueryTest.java
new file mode 100644
index 0000000..b96e7b4
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultimapQueryTest.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2021 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 com.google.common.truth.Truth.assertThat;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.testing.TestLifecycleOwner;
+import androidx.room.Room;
+import androidx.room.integration.testapp.MusicTestDatabase;
+import androidx.room.integration.testapp.dao.MusicDao;
+import androidx.room.integration.testapp.vo.Album;
+import androidx.room.integration.testapp.vo.AlbumNameAndBandName;
+import androidx.room.integration.testapp.vo.AlbumWithSongs;
+import androidx.room.integration.testapp.vo.Artist;
+import androidx.room.integration.testapp.vo.ReleasedAlbum;
+import androidx.room.integration.testapp.vo.Song;
+import androidx.sqlite.db.SimpleSQLiteQuery;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.hamcrest.MatcherAssert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import io.reactivex.Flowable;
+
+/**
+ * Tests multimap return type for JOIN statements.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class MultimapQueryTest {
+ // TODO: (b/191265082) Handle duplicate column names in JOINs
+ private MusicDao mMusicDao;
+
+ private final Song mSong1 = new Song(
+ 1,
+ "Dani California",
+ "Red Hot Chili Peppers",
+ "Stadium Arcadium",
+ 442,
+ 2006);
+ private final Song mSong2 = new Song(
+ 2,
+ "Snow (Hey Oh)",
+ "Red Hot Chili Peppers",
+ "Stadium Arcadium",
+ 514,
+ 2006);
+ private final Song mSong3 = new Song(
+ 3,
+ "Highway to Hell",
+ "AC/DC",
+ "Highway to Hell",
+ 328,
+ 1979);
+ private final Song mSong4 = new Song(
+ 4,
+ "The Great Gig in the Sky",
+ "Pink Floyd",
+ "The Dark Side of the Moon",
+ 443,
+ 1973);
+
+
+ private final Artist mRhcp = new Artist(
+ 1,
+ "Red Hot Chili Peppers"
+ );
+ private final Artist mAcDc = new Artist(
+ 2,
+ "AC/DC"
+ );
+ private final Artist mTheClash = new Artist(
+ 3,
+ "The Clash"
+ );
+ private final Artist mPinkFloyd = new Artist(
+ 4,
+ "Pink Floyd"
+ );
+
+ private final Album mStadiumArcadium = new Album(
+ 1,
+ "Stadium Arcadium",
+ "Red Hot Chili Peppers",
+ "2006"
+ );
+
+ private final Album mCalifornication = new Album(
+ 2,
+ "Californication",
+ "Red Hot Chili Peppers",
+ "1999"
+ );
+
+ private final Album mHighwayToHell = new Album(
+ 3,
+ "Highway to Hell",
+ "AC/DC",
+ "1979"
+ );
+
+ private final Album mTheDarkSideOfTheMoon = new Album(
+ 4,
+ "The Dark Side of the Moon",
+ "Pink Floyd",
+ "1973"
+ );
+
+ @Rule
+ public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
+
+ private void drain() throws TimeoutException, InterruptedException {
+ mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
+ assertThat(mExecutorRule.isIdle()).isTrue();
+ }
+
+ private class MyTestObserver<T> extends TestObserver<T> {
+ @Override
+ protected void drain() throws TimeoutException, InterruptedException {
+ MultimapQueryTest.this.drain();
+ }
+ }
+
+ @Before
+ public void createDb() {
+ Context context = ApplicationProvider.getApplicationContext();
+ MusicTestDatabase db = Room.inMemoryDatabaseBuilder(context, MusicTestDatabase.class)
+ .build();
+ mMusicDao = db.getDao();
+ }
+
+ /**
+ * Tests a simple JOIN query between two tables.
+ */
+ @Test
+ public void testJoinByArtistName() {
+ mMusicDao.addSongs(mSong1, mSong2, mSong3, mSong4);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+ Map<Artist, List<Song>> artistToSongsMap = mMusicDao.getAllArtistAndTheirSongs();
+ assertContentsOfResultMap(artistToSongsMap);
+ }
+
+ /**
+ * Tests a JOIN {@link androidx.room.RawQuery} between two tables.
+ */
+ @Test
+ public void testJoinByArtistNameRawQuery() {
+ mMusicDao.addSongs(mSong1, mSong2, mSong3, mSong4);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+ Map<Artist, List<Song>> artistToSongsMap = mMusicDao.getAllArtistAndTheirSongsRawQuery(
+ new SimpleSQLiteQuery(
+ "SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist"
+ )
+ );
+ assertContentsOfResultMap(artistToSongsMap);
+ }
+
+ /**
+ * Tests a simple JOIN query between two tables with a {@link LiveData} return type.
+ */
+ @Test
+ public void testJoinByArtistNameLiveData()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ mMusicDao.addSongs(mSong1, mSong2, mSong3, mSong4);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+ LiveData<Map<Artist, List<Song>>> artistToSongsMapLiveData =
+ mMusicDao.getAllArtistAndTheirSongsAsLiveData();
+ final TestLifecycleOwner testOwner = new TestLifecycleOwner(Lifecycle.State.CREATED);
+ final TestObserver<Map<Artist, List<Song>>> observer = new MyTestObserver<>();
+ TestUtil.observeOnMainThread(artistToSongsMapLiveData, testOwner, observer);
+ MatcherAssert.assertThat(observer.hasValue(), is(false));
+ observer.reset();
+ testOwner.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(observer.get()).isNotNull();
+ assertContentsOfResultMap(observer.get());
+ }
+
+ /**
+ * Tests a simple JOIN query between two tables with a {@link Flowable} return type.
+ */
+ @Test
+ public void testJoinByArtistNameFlowable() {
+ mMusicDao.addSongs(mSong1, mSong2, mSong3, mSong4);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+ Flowable<Map<Artist, List<Song>>> artistToSongsMapFlowable =
+ mMusicDao.getAllArtistAndTheirSongsAsFlowable();
+ assertContentsOfResultMap(artistToSongsMapFlowable.blockingFirst());
+ }
+
+ /**
+ * Tests a simple JOIN query between two tables with a return type of a map with a key that
+ * is an entity {@link Artist} and a list of entity POJOs {@link AlbumWithSongs} that use
+ * {@link androidx.room.Embedded} and {@link androidx.room.Relation}.
+ */
+ @Test
+ public void testPojoWithEmbeddedAndRelation() {
+ mMusicDao.addSongs(mSong1, mSong2, mSong3, mSong4);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+ mMusicDao.addAlbums(
+ mStadiumArcadium,
+ mCalifornication,
+ mTheDarkSideOfTheMoon,
+ mHighwayToHell
+ );
+
+ Map<Artist, List<AlbumWithSongs>> artistToAlbumsWithSongsMap =
+ mMusicDao.getAllArtistAndTheirAlbumsWithSongs();
+ List<AlbumWithSongs> rhcpList = artistToAlbumsWithSongsMap.get(mRhcp);
+
+ assertThat(artistToAlbumsWithSongsMap.keySet()).containsExactlyElementsIn(
+ Arrays.asList(mRhcp, mAcDc, mPinkFloyd));
+ assertThat(artistToAlbumsWithSongsMap.containsKey(mTheClash)).isFalse();
+ assertThat(artistToAlbumsWithSongsMap.get(mPinkFloyd).get(0).getAlbum())
+ .isEqualTo(mTheDarkSideOfTheMoon);
+ assertThat(artistToAlbumsWithSongsMap.get(mAcDc).get(0).getAlbum())
+ .isEqualTo(mHighwayToHell);
+ assertThat(artistToAlbumsWithSongsMap.get(mAcDc).get(0).getSongs().get(0))
+ .isEqualTo(mSong3);
+
+ for (AlbumWithSongs albumAndSong : rhcpList) {
+ if (albumAndSong.getAlbum().equals(mStadiumArcadium)) {
+ assertThat(albumAndSong.getSongs()).containsExactlyElementsIn(
+ Arrays.asList(mSong1, mSong2)
+ );
+ } else if (albumAndSong.getAlbum().equals(mCalifornication)) {
+ assertThat(albumAndSong.getSongs()).isEmpty();
+ } else {
+ fail();
+ }
+ }
+ }
+
+ /**
+ * Tests a simple JOIN query between two tables with a return type of a map with a key
+ * {@link ReleasedAlbum} and value (list of {@link AlbumNameAndBandName}) that are non-entity
+ * POJOs.
+ */
+ @Test
+ public void testNonEntityPojos() {
+ mMusicDao.addSongs(mSong1, mSong2, mSong3, mSong4);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+ mMusicDao.addAlbums(
+ mStadiumArcadium,
+ mCalifornication,
+ mTheDarkSideOfTheMoon,
+ mHighwayToHell
+ );
+
+ Map<ReleasedAlbum, List<AlbumNameAndBandName>> map =
+ mMusicDao.getReleaseYearToAlbumsAndBands();
+ Set<ReleasedAlbum> allReleasedAlbums = map.keySet();
+
+ assertThat(allReleasedAlbums.size()).isEqualTo(3);
+
+ for (ReleasedAlbum album : allReleasedAlbums) {
+ if (album.getAlbumName().equals(mStadiumArcadium.mAlbumName)) {
+ assertThat(album.getReleaseYear()).isEqualTo(mStadiumArcadium.mAlbumReleaseYear);
+ assertThat(map.get(album).size()).isEqualTo(2);
+ assertThat(map.get(album).get(0).getBandName()).isEqualTo(mRhcp.mArtistName);
+ assertThat(map.get(album).get(0).getAlbumName())
+ .isEqualTo(mStadiumArcadium.mAlbumName);
+ assertThat(map.get(album).get(1).getBandName()).isEqualTo(mRhcp.mArtistName);
+ assertThat(map.get(album).get(1).getAlbumName())
+ .isEqualTo(mStadiumArcadium.mAlbumName);
+
+ } else if (album.getAlbumName().equals(mHighwayToHell.mAlbumName)) {
+ assertThat(album.getReleaseYear()).isEqualTo(mHighwayToHell.mAlbumReleaseYear);
+ assertThat(map.get(album).size()).isEqualTo(1);
+ assertThat(map.get(album).get(0).getBandName()).isEqualTo(mAcDc.mArtistName);
+ assertThat(map.get(album).get(0).getAlbumName())
+ .isEqualTo(mHighwayToHell.mAlbumName);
+
+ } else if (album.getAlbumName().equals(mTheDarkSideOfTheMoon.mAlbumName)) {
+ assertThat(album.getReleaseYear())
+ .isEqualTo(mTheDarkSideOfTheMoon.mAlbumReleaseYear);
+ assertThat(map.get(album).size()).isEqualTo(1);
+ assertThat(map.get(album).get(0).getBandName())
+ .isEqualTo(mPinkFloyd.mArtistName);
+ assertThat(map.get(album).get(0).getAlbumName())
+ .isEqualTo(mTheDarkSideOfTheMoon.mAlbumName);
+
+ } else {
+ // Shouldn't get here as we expect only the 3 albums to be keys in the map
+ fail();
+ }
+ }
+ }
+
+ /**
+ * Checks that the contents of the map are as expected.
+ *
+ * @param artistToSongsMap Map of Artists to Songs joined by the artist name
+ */
+ private void assertContentsOfResultMap(Map<Artist, List<Song>> artistToSongsMap) {
+ assertThat(artistToSongsMap.keySet()).containsExactlyElementsIn(
+ Arrays.asList(mRhcp, mAcDc, mPinkFloyd));
+ assertThat(artistToSongsMap.containsKey(mTheClash)).isFalse();
+ assertThat(artistToSongsMap.get(mPinkFloyd)).containsExactly(mSong4);
+ assertThat(artistToSongsMap.get(mRhcp)).containsExactlyElementsIn(
+ Arrays.asList(mSong1, mSong2)
+ );
+ assertThat(artistToSongsMap.get(mAcDc)).containsExactly(mSong3);
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Album.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Album.java
new file mode 100644
index 0000000..4c445d4
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Album.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 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 Album {
+ @PrimaryKey
+ public final int mAlbumId;
+ public final String mAlbumName;
+ public final String mAlbumArtist;
+ public final String mAlbumReleaseYear;
+
+ public Album(int albumId, String albumName, String albumArtist, String albumReleaseYear) {
+ mAlbumId = albumId;
+ mAlbumName = albumName;
+ mAlbumArtist = albumArtist;
+ mAlbumReleaseYear = albumReleaseYear;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Album album = (Album) o;
+
+ if (mAlbumId != album.mAlbumId) return false;
+ if (mAlbumName != null ? !mAlbumName.equals(album.mAlbumName) :
+ album.mAlbumName != null) {
+ return false;
+ }
+ if (mAlbumArtist != null ? !mAlbumArtist.equals(album.mAlbumArtist) :
+ album.mAlbumArtist != null) {
+ return false;
+ }
+ if (mAlbumReleaseYear != null ? !mAlbumReleaseYear.equals(album.mAlbumReleaseYear) :
+ album.mAlbumReleaseYear != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mAlbumId;
+ result = 31 * result + (mAlbumName != null ? mAlbumName.hashCode() : 0);
+ result = 31 * result + (mAlbumArtist != null ? mAlbumArtist.hashCode() : 0);
+ result = 31 * result + (mAlbumReleaseYear != null ? mAlbumReleaseYear.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/AlbumNameAndBandName.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/AlbumNameAndBandName.java
new file mode 100644
index 0000000..854c0bf
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/AlbumNameAndBandName.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.testapp.vo;
+
+
+public class AlbumNameAndBandName {
+ private String mAlbumName;
+ private String mBandName;
+
+ public AlbumNameAndBandName(String albumName, String bandName) {
+ mAlbumName = albumName;
+ mBandName = bandName;
+ }
+
+ public String getAlbumName() {
+ return mAlbumName;
+ }
+
+ public String getBandName() {
+ return mBandName;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ AlbumNameAndBandName that = (AlbumNameAndBandName) o;
+
+ if (mAlbumName != null ? !mAlbumName.equals(that.mAlbumName) :
+ that.mAlbumName != null) {
+ return false;
+ }
+ return mBandName != null ? mBandName.equals(that.mBandName) : that.mBandName == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mAlbumName != null ? mAlbumName.hashCode() : 0;
+ result = 31 * result + (mBandName != null ? mBandName.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/AlbumWithSongs.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/AlbumWithSongs.java
new file mode 100644
index 0000000..980fb01
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/AlbumWithSongs.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2021 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.Relation;
+
+import java.util.List;
+
+public class AlbumWithSongs {
+ @Embedded
+ private final Album mAlbum;
+
+ @Relation(parentColumn = "mAlbumName", entityColumn = "mAlbum")
+ private final List<Song> mSongs;
+
+ public AlbumWithSongs(Album album,
+ List<Song> songs) {
+ this.mAlbum = album;
+ this.mSongs = songs;
+ }
+
+ public Album getAlbum() {
+ return mAlbum;
+ }
+
+ public List<Song> getSongs() {
+ return mSongs;
+ }
+}
+
+
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Artist.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Artist.java
new file mode 100644
index 0000000..603cde0
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Artist.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 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 Artist {
+ @PrimaryKey
+ public final int mArtistId;
+ public final String mArtistName;
+
+ public Artist(int artistId, String artistName) {
+ mArtistId = artistId;
+ mArtistName = artistName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Artist artist = (Artist) o;
+
+ if (mArtistId != artist.mArtistId) return false;
+ if (mArtistName != null ? !mArtistName.equals(artist.mArtistName) :
+ artist.mArtistName != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mArtistId;
+ result = 31 * result + (mArtistName != null ? mArtistName.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/ReleasedAlbum.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/ReleasedAlbum.java
new file mode 100644
index 0000000..c79e8a2
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/ReleasedAlbum.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.testapp.vo;
+
+
+public class ReleasedAlbum {
+ private String mReleaseYear;
+ private String mAlbumName;
+
+ public ReleasedAlbum(String releaseYear, String albumName) {
+ mReleaseYear = releaseYear;
+
+ mAlbumName = albumName;
+ }
+
+ public String getReleaseYear() {
+ return mReleaseYear;
+ }
+
+ public String getAlbumName() {
+ return mAlbumName;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ReleasedAlbum that = (ReleasedAlbum) o;
+ if (mReleaseYear != null ? !mReleaseYear.equals(that.mReleaseYear) :
+ that.mReleaseYear != null) {
+ return false;
+ }
+ return mAlbumName != null ? mAlbumName.equals(that.mAlbumName) : that.mAlbumName == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mReleaseYear != null ? mReleaseYear.hashCode() : 0;
+ result = 31 * result + (mAlbumName != null ? mAlbumName.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 127d0fe..5147586 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -57,6 +57,7 @@
import androidx.room.solver.query.result.GuavaOptionalQueryResultAdapter
import androidx.room.solver.query.result.ImmutableListQueryResultAdapter
import androidx.room.solver.query.result.ListQueryResultAdapter
+import androidx.room.solver.query.result.MapQueryResultAdapter
import androidx.room.solver.query.result.OptionalQueryResultAdapter
import androidx.room.solver.query.result.PojoRowAdapter
import androidx.room.solver.query.result.QueryResultAdapter
@@ -461,6 +462,26 @@
typeArg = typeArg,
rowAdapter = rowAdapter
)
+ } else if (typeMirror.isTypeOf(java.util.Map::class)) {
+ val keyArg = typeMirror.typeArguments[0].extendsBoundOrSelf()
+ val secondTypeArg = typeMirror.typeArguments[1].extendsBoundOrSelf()
+
+ // TODO: Support Set::class here as well.
+ if (!secondTypeArg.isTypeOf(java.util.List::class)) {
+ context.logger.e("Only supporting Map<Key, List<Value>> for now.")
+ return null
+ }
+ val valueArg = secondTypeArg.typeArguments.first().extendsBoundOrSelf()
+
+ val keyRowAdapter = findRowAdapter(keyArg, query) ?: return null
+ val valueRowAdapter = findRowAdapter(valueArg, query) ?: return null
+
+ return MapQueryResultAdapter(
+ keyTypeArg = keyArg,
+ valueTypeArg = valueArg,
+ keyRowAdapter = keyRowAdapter,
+ valueRowAdapter = valueRowAdapter
+ )
}
return null
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
new file mode 100644
index 0000000..520b1de
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2021 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.solver.query.result
+
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.ParameterizedTypeName
+
+class MapQueryResultAdapter(
+ private val keyTypeArg: XType,
+ private val valueTypeArg: XType,
+ private val keyRowAdapter: RowAdapter,
+ private val valueRowAdapter: RowAdapter,
+) : QueryResultAdapter(null) {
+ private val listType = ParameterizedTypeName.get(
+ ClassName.get(List::class.java),
+ valueTypeArg.typeName
+ )
+
+ private val arrayListType = ParameterizedTypeName
+ .get(ClassName.get(ArrayList::class.java), valueTypeArg.typeName)
+
+ private val mapType = ParameterizedTypeName.get(
+ ClassName.get(Map::class.java),
+ keyTypeArg.typeName,
+ listType
+ )
+
+ private val hashMapType = ParameterizedTypeName.get(
+ ClassName.get(HashMap::class.java),
+ keyTypeArg.typeName,
+ listType
+ )
+
+ override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+ scope.builder().apply {
+ keyRowAdapter.onCursorReady(cursorVarName, scope)
+ valueRowAdapter.onCursorReady(cursorVarName, scope)
+ addStatement(
+ "final $T $L = new $T()",
+ mapType, outVarName, hashMapType
+ )
+ val tmpKeyVarName = scope.getTmpVar("_key")
+ val tmpValueVarName = scope.getTmpVar("_value")
+ beginControlFlow("while ($L.moveToNext())", cursorVarName).apply {
+ addStatement("final $T $L", keyTypeArg.typeName, tmpKeyVarName)
+ keyRowAdapter.convert(tmpKeyVarName, cursorVarName, scope)
+
+ addStatement("final $T $L", valueTypeArg.typeName, tmpValueVarName)
+ valueRowAdapter.convert(tmpValueVarName, cursorVarName, scope)
+
+ val tmpListVarName = scope.getTmpVar("_values")
+ addStatement("$T $L", listType, tmpListVarName)
+ beginControlFlow("if ($L.containsKey($L))", outVarName, tmpKeyVarName).apply {
+ addStatement("$L = $L.get($L)", tmpListVarName, outVarName, tmpKeyVarName)
+ }
+ nextControlFlow("else").apply {
+ addStatement("$L = new $T()", tmpListVarName, arrayListType)
+ addStatement("$L.put($L, $L)", outVarName, tmpKeyVarName, tmpListVarName)
+ }
+ endControlFlow()
+ addStatement("$L.add($L)", tmpListVarName, tmpValueVarName)
+ }
+ endControlFlow()
+ keyRowAdapter.onCursorFinished()?.invoke(scope)
+ valueRowAdapter.onCursorFinished()?.invoke(scope)
+ }
+ }
+
+ override fun shouldCopyCursor() =
+ (keyRowAdapter is PojoRowAdapter && keyRowAdapter.relationCollectors.isNotEmpty()) ||
+ (valueRowAdapter is PojoRowAdapter && valueRowAdapter.relationCollectors.isNotEmpty())
+
+ override fun accessedTableNames() = mutableListOf<String>().apply {
+ (keyRowAdapter as? PojoRowAdapter)?.relationTableNames()?.let { addAll(it) }
+ (valueRowAdapter as? PojoRowAdapter)?.relationTableNames()?.let { addAll(it) }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt
index dc27eaf..68b5242 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt
@@ -29,7 +29,7 @@
open fun shouldCopyCursor() = rowAdapter is PojoRowAdapter &&
rowAdapter.relationCollectors.isNotEmpty()
- fun accessedTableNames(): List<String> {
+ open fun accessedTableNames(): List<String> {
return (rowAdapter as? PojoRowAdapter)?.relationTableNames() ?: emptyList()
}
}