Polymorphism With a Single Table
We could go the other route: have a single table for all note objects, regardless of whether they are a comment or a link. For small objects with few properties, with a lot of overlap between the properties of the concrete types, this is manageable. It becomes unwieldy for many concrete types with many disparate properties. It also puts limits on your SQL, as the only practical NOT NULL
columns are ones for which you can supply values for every possible concrete type. You also need some way of determining what concrete type to use for any given table row, and often that requires yet another column.
But, it is an option.
The polysingle
sub-package in the MiscSamples
module demonstrates this approach. This time, the entity is NoteEntity
:
package com.commonsware.room.misc.polysingle
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "notes")
open class NoteEntity(
@PrimaryKey(autoGenerate = true)
val id: Long,
val title: String,
var url: String?
)
It contains a superset of all columns from both our comments and links. In this case, that just means that the URL is optional — a link has one, but a comment does not. This approach will get a lot more messy if you have lots of entities and lots of columns that exist only in a subset of those entity types, though.
Now, Comment
and Link
are subclasses of NoteEntity
, implementing the Note
interface from the poly
package and overriding displayText
as needed for their scenarios:
package com.commonsware.room.misc.polysingle
import com.commonsware.room.misc.poly.Note
class Comment(id: Long, title: String) : NoteEntity(id, title, null), Note {
override val displayText: CharSequence
get() = title
}
package com.commonsware.room.misc.polysingle
import androidx.core.text.HtmlCompat
import com.commonsware.room.misc.poly.Note
class Link(
id: Long,
title: String,
url: String
) : NoteEntity(id, title, url), Note {
override val displayText: CharSequence
get() = HtmlCompat.fromHtml(
"""<a href="$url">$title</a>""",
HtmlCompat.FROM_HTML_MODE_COMPACT
)
}
You might argue that NoteEntity
should be abstract
and define displayText
there. That could work, at the cost of not being able to load NoteEntity
objects directly, as Room cannot create instances of an abstract
class.
PolySingleStore
— the DAO for this scenario — is a bit simpler. We do not need dedicated insert()
functions for Link
and Comment
, as an insert()
function for NoteEntity
covers both of those cases. allLinks()
and allComments()
can take advantage of Room’s return type flexibility, having Room create Link
and Comment
objects directly, with our query returning the proper rows based on whether we have a url
or not:
package com.commonsware.room.misc.polysingle
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface PolySingleStore {
@Query("SELECT * FROM notes")
fun allNotes(): List<NoteEntity>
@Insert
fun insert(vararg notes: NoteEntity)
@Query("SELECT * FROM notes WHERE url IS NOT NULL")
fun allLinks(): List<Link>
@Query("SELECT id, title FROM notes WHERE url IS NULL")
fun allComments(): List<Comment>
}
And, once again, we have sufficient CRUD operations now to be able to manipulate links and comments separately or treating them all as notes:
package com.commonsware.room.misc.polysingle
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.commonsware.room.misc.MiscDatabase
import com.natpryce.hamkrest.anyOf
import com.natpryce.hamkrest.assertion.assertThat
import com.natpryce.hamkrest.equalTo
import com.natpryce.hamkrest.hasSize
import com.natpryce.hamkrest.isEmpty
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PolySingleStoreTest {
private val db = Room.inMemoryDatabaseBuilder(
InstrumentationRegistry.getInstrumentation().targetContext,
MiscDatabase::class.java
)
.build()
private val underTest = db.polySingleStore()
@Test
fun comments() {
assertThat(underTest.allComments(), isEmpty)
val firstComment = Comment(1, "This is a comment")
val secondComment = Comment(2, "This is another comment")
underTest.insert(firstComment, secondComment)
val allComments = underTest.allComments()
assertThat(allComments, hasSize(equalTo(2)))
assertThat(
allComments[0].title,
anyOf(equalTo(firstComment.title), equalTo(secondComment.title))
)
assertThat(
allComments[1].title,
anyOf(equalTo(firstComment.title), equalTo(secondComment.title))
)
val allNotes = underTest.allNotes()
assertThat(allNotes, hasSize(equalTo(2)))
assertThat(
allNotes[0].title,
anyOf(equalTo(firstComment.title), equalTo(secondComment.title))
)
assertThat(
allNotes[1].title,
anyOf(equalTo(firstComment.title), equalTo(secondComment.title))
)
}
@Test
fun links() {
assertThat(underTest.allLinks(), isEmpty)
val firstLink = Link(1, "CommonsWare", "https://commonsware.com")
val secondLink = Link(
2,
"Room Release Notes",
"https://developer.android.com/jetpack/androidx/releases/room"
)
underTest.insert(firstLink, secondLink)
val allLinks = underTest.allLinks()
assertThat(allLinks, hasSize(equalTo(2)))
assertThat(
allLinks[0].title,
anyOf(equalTo(firstLink.title), equalTo(secondLink.title))
)
assertThat(
allLinks[1].title,
anyOf(equalTo(firstLink.title), equalTo(secondLink.title))
)
val allNotes = underTest.allNotes()
assertThat(allNotes, hasSize(equalTo(2)))
assertThat(
allNotes[0].title,
anyOf(equalTo(firstLink.title), equalTo(secondLink.title))
)
assertThat(
allNotes[1].title,
anyOf(equalTo(firstLink.title), equalTo(secondLink.title))
)
}
@Test
fun notes() {
assertThat(underTest.allNotes(), isEmpty)
val firstComment = Comment(1, "This is a comment")
val secondComment = Comment(2, "This is another comment")
val firstLink = Link(3, "CommonsWare", "https://commonsware.com")
val secondLink = Link(
4,
"Room Release Notes",
"https://developer.android.com/jetpack/androidx/releases/room"
)
underTest.insert(firstComment, secondComment, firstLink, secondLink)
val allNotes = underTest.allNotes()
assertThat(allNotes, hasSize(equalTo(4)))
assertThat(
allNotes[0].title,
anyOf(
equalTo(firstComment.title),
equalTo(secondComment.title),
equalTo(firstLink.title),
equalTo(secondLink.title)
)
)
assertThat(
allNotes[1].title,
anyOf(
equalTo(firstComment.title),
equalTo(secondComment.title),
equalTo(firstLink.title),
equalTo(secondLink.title)
)
)
assertThat(
allNotes[2].title,
anyOf(
equalTo(firstComment.title),
equalTo(secondComment.title),
equalTo(firstLink.title),
equalTo(secondLink.title)
)
)
assertThat(
allNotes[3].title,
anyOf(
equalTo(firstComment.title),
equalTo(secondComment.title),
equalTo(firstLink.title),
equalTo(secondLink.title)
)
)
}
}
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.