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, if having multiple tables makes you concerned.

The Trips/RoomPolySingle sample project employs this strategy. It has the same structure for Comment, Link, and Note, but this time Note is the @Entity:

package com.commonsware.android.room;

import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import android.support.annotation.NonNull;
import static android.arch.persistence.room.ForeignKey.CASCADE;

@Entity(
  tableName="notes",
  foreignKeys=@ForeignKey(
    entity=Trip.class,
    parentColumns="id",
    childColumns="tripId",
    onDelete=CASCADE),
  indices=@Index("tripId"))
public class Note {
  public enum Type {
    COMMENT(0),
    LINK(1);

    private final int value;

    Type(int value) {
      this.value=value;
    }

    public int value() {
      return value;
    }
  }

  @PrimaryKey
  @NonNull
  public final String id;

  public final String title;
  public final String url;
  @NonNull public final String tripId;
  public final Type type;

  public Note(@NonNull String id, String title, @NonNull String url,
              @NonNull String tripId, Type type) {
    this.id=id;
    this.title=title;
    this.url=url;
    this.tripId=tripId;
    this.type=type;
  }
}

In addition to the fields from Link and Comment, we also have a type field, housing an enum that indicates whether this Note is a LINK or COMMENT. This requires @TypeConverter methods, in this case added to the existing TypeTransmogrifier class:

  @TypeConverter
  public static Integer fromType(Note.Type type) {
    return type.value();
  }

  @TypeConverter
  public static Note.Type toType(Integer value) {
    return value==0 ? Note.Type.COMMENT : Note.Type.LINK;
  }

Comment is a subclass of Note, using the title field to hold the comment text:

package com.commonsware.android.room;

import android.arch.persistence.room.Ignore;
import android.support.annotation.NonNull;
import java.util.UUID;

public class Comment extends Note {
  public Comment(@NonNull String id, String title, String url,
                 @NonNull String tripId, Type type) {
    super(id, title, url, tripId, Type.COMMENT);
  }

  @Ignore
  public Comment(String text, @NonNull Trip trip) {
    this(UUID.randomUUID().toString(), text, null, trip.id, Type.COMMENT);
  }
}

Link is another subclass of Note:

package com.commonsware.android.room;

import android.arch.persistence.room.Ignore;
import android.support.annotation.NonNull;
import java.util.UUID;

public class Link extends Note {
  public Link(@NonNull String id, String title, String url,
                 @NonNull String tripId, Type type) {
    super(id, title, url, tripId, Type.LINK);
  }

  @Ignore
  public Link(String text, String url, @NonNull Trip trip) {
    this(UUID.randomUUID().toString(), text, url, trip.id, Type.LINK);
  }
}

This simplifies our @Dao class (TripStore). In effect, Room ignores the difference between Link and Comment, dealing only with the base Note class, since that is the @Entity. So for inserts, updates, and deletes, we can pass a Link or Comment to methods that take a Note, and it all works fine.

  /*
    Note
   */

  @Query("SELECT * FROM notes WHERE tripId=:tripId")
  abstract List<Note> findNotesForTrip(String tripId);

  @Insert
  abstract void insert(Note... comments);

  @Update
  abstract void update(Note... comments);

  @Delete
  abstract void delete(Note... comments);

Retrieval becomes a bit more interesting, though. findNotesForTrip(), shown above, nicely returns all links and comments… but as Note objects, not as Link and Comment objects. If we want those, we need to have dedicated retrieval methods by type:

  /*
    Comment
   */

  @Query("SELECT * FROM notes WHERE tripId=:tripId AND type=0")
  abstract List<Comment> findCommentsForTrip(String tripId);

  /*
    Link
   */

  @Query("SELECT * FROM notes WHERE tripId=:tripId AND type=1")
  abstract List<Link> findLinksForTrip(String tripId);

And, as a result, we do not have a single method to retrieve both links and comments as Link and Comment objects. We would need another @Transaction wrapper method as before.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.