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.