Reputation: 49
I'm creating a simple app to practice working with databases. The app has Playlists and Songs. Each Playlist contains many songs, and each Song can be in many Playlists. So it will need a many-to-many relation.
I'm trying to stick to Android's Activity->ViewModel->Repository->Database architecture using LiveData and Room.
The app's MainActivity gathers two song names and a playlist name from the user, then adds them to the Room database when the button is clicked.
Here's the Playlist object, the Song object, plus an extra object to use as a cross reference, CrossRef:
Playlist.class:
@Entity(tableName="playlist_table")
public class Playlist {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name="playlist_id")
private int playlistId;
private String name = "Default Playlist";
}
Song.class:
@Entity(tableName="song_table")
public class Song {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name="song_id")
private int songId;
private String name;
}
CrossRef.class:
@Entity(tableName="cross_ref_table", primaryKeys = {"playlist_id", "song_id"})
public class CrossRef {
@ColumnInfo(index = true, name = "playlist_id")
public int playlistId;
@ColumnInfo(index = true, name = "song_id")
public int songId;
public CrossRef(int playlistId, int songId) {
this.playlistId = playlistId;
this.songId = songId;
}
}
MainActivity gets the data from the user, calls MyViewModel to insert the data
MainActivity.class:
myViewModel.insert(playlist, songs);
then MyViewModel uses its Repository to:
MyViewModel.class:
public void insert(Playlist playlist, List<Song> newSongs) {
int playlistId = (int)localRepository.insert(playlist);
int songId = 0;
for (Song song: newSongs) {
if(!songExists(song)) {
songId = (int)localRepository.insert(song);
} else {
songId = song.getSongId();
}
CrossRef crossRef = new CrossRef(playlistId, songId);
localRepository.insert(crossRef);
}
}
The Repository then calls the Dao to do the actual work. LocalRepository.class:
public long insert(Playlist playlist){
new InsertPlaylistAsyncTask(myDao).execute(playlist);
return resultId; // Once the async task is done, return the new playlistId.
}
public long insert(Song song){
new InsertSongAsyncTask(myDao).execute(song);
return resultId; // Once the async task is done, return the new songId.
}
public void insert(CrossRef crossRef){
new InsertCrossRefAsyncTask(myDao).execute(crossRef);
}
MyDao:
@Insert
long insert(Playlist playlist); // returns a long value of the newly inserted playlistId.
@Insert
long insert(Song song); // returns a long value of the newly inserted songId.
@Insert
void insert(CrossRef crossRef);
The issue I am running into is getting autogenerated id's. They always come back as 0! In MyDao, this line should assign playlistId to the newly inserted playlist ID, right?
int playlistId = (int)localRepository.insert(playlist);
But no, it's always zero. Here's the InsertPlaylistAsyncTask in the Repository where the new id SHOULD be passed back onPostExecute:
private class InsertPlaylistAsyncTask extends AsyncTask<Playlist, Void, Long> {
private MyDao myDao;
private InsertPlaylistAsyncTask(MyDao myDao){
this.myDao = myDao;
}
@Override
protected Long doInBackground(Playlist... playlists) {
long id = 0; // TODO: make this an array and return an ArrayList<Long>
for (Playlist r:playlists) {
id = myDao.insert(r);
}
return id;
}
@Override
protected void onPostExecute(Long playlistId) {
resultId = playlistId;
}
}
If anyone has a good resource to learn more about INSERTing to a database with many-to-many relations, I'm all ears! Thanks all.
Upvotes: 2
Views: 558
Reputation: 49
I think I found my own solution. Use a handler!
Create an object PlaylistWithSongs. Call the repository to insertPlaylistWithSongs.
Now insert the Playlists and Songs in their tables: The repository inserts the Playlist in a second AsyncTask which calls a handler on the main thread with the playlistId in hand. The repository inserts each song in the list in more AsyncTasks, calling the same handler with each new songId generated.
The handler is on the main thread, waiting for ids to roll in. Every time it sees a valid playlistId and a valid songId, it inserts the match into the crossRef table.
PlaylistWithSongs.java:
public class PlaylistWithSongs {
@Embedded
public Playlist playlist;
// The songs variable will contain all songs related by the playlistId and songId.
@Relation(
parentColumn = "playlist_id",
entityColumn = "song_id",
associateBy = @Junction(CrossRef.class)
)
public List<Song> songs;
}
LocalRepository.java:
public void insertPlaylistWithSongs(PlaylistWithSongs playlistWithSongs) {
MyDao myDao;
insertPlaylist(playlistWithSongs.getPlaylist());
for (Song song : playlistWithSongs.getSongs()) {
insertSong(song);
}
tempPlaylistId = 0;
tempSongId = 0;
}
insertPlaylist inserts the playlist in the table and saves the new id id = (int)myDao.insert(playlist);
. It packages a message containing the new id ids.putInt(PLAYLISTID, id);
to send to the handler on the main thread handler.sendMessage(msg);
:
public void insertPlaylist(Playlist playlist){
new InsertPlaylistAsyncTask(myDao).execute(playlist);
}
private class InsertPlaylistAsyncTask extends AsyncTask<Playlist, Void, Void> {
private MyDao myDao;
private InsertPlaylistAsyncTask(MyDao myDao){
this.myDao = myDao;
}
@Override
protected Void doInBackground(Playlist... playlists) {
int id = 0;
for (Playlist playlist:playlists) {
Playlist retrievedFromDatabase = myDao.getPlaylistByName(playlist.getName());
if(retrievedFromDatabase == null){
// If playlist name doesn't exist
id = (int)myDao.insert(playlist);
} else {
// Else set the id to the existing playlist id
id = retrievedFromDatabase.getPlaylistId();
}
// Pass the new id to the main thread once the background insert is complete.
Bundle ids = new Bundle();
ids.putInt(PLAYLISTID, id);
Message msg = new Message();
msg.setData(ids);
msg.setTarget(handler);
handler.sendMessage(msg);
}
return null;
}
}
Each song is handled the exact same way, so I won't repeat the code.
Each time the handler receives a new id, it updates the Repository's static variables, private static int tempSongId = 0;
and private static int tempPlaylistId = 0;
, then checks if it has enough valid information to insert the crossRef:
Handler handler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
Bundle ids = msg.getData();
if(ids.getInt(PLAYLISTID) != 0){
tempPlaylistId = msg.getData().getInt(PLAYLISTID);
}
if(ids.getInt(SONGID) != 0){
tempSongId = msg.getData().getInt(SONGID);
}
if(tempPlaylistId!=0 && tempSongId!=0) {
// if both ids were retrieved
CrossRef crossRef = new CrossRef(tempPlaylistId, tempSongId);
Log.d(TAG, "handler insert(crossRef): " + tempPlaylistId + "," + tempSongId);
insertCrossRef(crossRef);
}else{
// if not, do nothing. We need both ids to insert a crossRef entry.
}
}
};
One issue with this code: If the main activity stops before the AsyncTasks are complete, a memory leak could occur. Perhaps adding a weak reference could fix that problem.
Upvotes: 0