Plans for Trips

Let’s explore how @ForeignKey works by adding some more entities to the trip-tracking code, as seen in the Trips/RoomRelations sample project.

The Domain Model

In the beginning, we had just the Trip entity. However, a trip is made up of lots of pieces, so in this sample, we add two more: flights and lodgings. Not surprisingly, these come in the form of Flight and Lodging entity classes. A Trip can have zero or more related Flight instances and zero or more related Lodging instances.

However, many of the pieces of data that we need to track for these things — title, duration, start time, etc. — are in common. So, we will pull those things into an abstract base class named Plan, from which Trip, Flight, and Lodging will all inherit.

The New Entities

As a result, Plan itself is pretty much what Trip used to be:

package com.commonsware.android.room;

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

abstract class Plan {
  @PrimaryKey
  @NonNull
  public final String id;

  public final String title;
  public final int duration;

  @TypeConverters({Priority.class})
  public final Priority priority;

  public final Date startTime;
  public final Date creationTime;
  public final Date updateTime;

  @Ignore
  Plan(String title, int duration, Priority priority, Date startTime) {
    this(UUID.randomUUID().toString(), title, duration, priority, startTime,
      null, null);
  }

  Plan(String id, String title, int duration, Priority priority,
       Date startTime, Date creationTime, Date updateTime) {
    this.id=id;
    this.title=title;
    this.duration=duration;
    this.priority=priority;
    this.startTime=startTime;
    this.creationTime=creationTime;
    this.updateTime=updateTime;
  }

  @Override
  public String toString() {
    return(title);
  }
}

Note that while we have the Priority TypeConverter registered for the Priority field, we do not have the TypeTransmogrifier registered on the Plan class, the way we had it for Trip. That is due to a limitation in Room, whereby class-level @TypeConverters annotations are not inherited, though field-level ones are.

Instead, the TypeTransmogrifier @TypeConverters annotation appears on our rump Trip class:

package com.commonsware.android.room;

import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.PrimaryKey;
import android.arch.persistence.room.TypeConverters;
import java.util.Date;
import java.util.UUID;

@Entity(tableName = "trips")
@TypeConverters({TypeTransmogrifier.class})
class Trip extends Plan {
  @Ignore
  Trip(String title, int duration, Priority priority, Date startTime) {
    super(title, duration, priority, startTime);
  }

  Trip(String id, String title, int duration,
       Priority priority, Date startTime, Date creationTime,
       Date updateTime) {
    super(id, title, duration, priority, startTime, creationTime, updateTime);
  }
}

The relations that we are setting up from Trip to Flight and Lodging are 1:N relations. As such, the parent (Trip) does not need any foreign keys. Those are held by the children of the relation… such as Lodging:

package com.commonsware.android.room;

import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.TypeConverters;
import java.util.Date;
import static android.arch.persistence.room.ForeignKey.CASCADE;

@Entity(
  tableName="lodgings",
  foreignKeys=@ForeignKey(
    entity=Trip.class,
    parentColumns="id",
    childColumns="tripId",
    onDelete=CASCADE),
  indices=@Index("tripId"))
@TypeConverters({TypeTransmogrifier.class})
class Lodging extends Plan {
  public final String address;
  public final String tripId;

  @Ignore
  Lodging(String title, int duration, Priority priority, Date startTime,
          String address, String tripId) {
    super(title, duration, priority, startTime);
    this.address=address;
    this.tripId=tripId;
  }

  Lodging(String id, String title, int duration,
          Priority priority, Date startTime, Date creationTime,
          Date updateTime, String address, String tripId) {
    super(id, title, duration, priority, startTime, creationTime, updateTime);
    this.address=address;
    this.tripId=tripId;
  }
}

Here, Lodging also extends from Plan, adding two fields, one to track the address of the hotel (or whatever) and the tripId of the Trip that contains this Lodging. That tripId field is then referenced in the @ForeignKey annotation,which:

Lodging also sets up an index on tripId (indices=@Index("tripId")). Querying on tripId will be fairly common, as we look up the Lodging instances associated with a given Trip. Hence, typically you will want to set up an index on your foreign keys. Room will even warn you about this, if you examine the Gradle Console output from a build.

Flight works similarly:

package com.commonsware.android.room;

import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.TypeConverters;
import java.util.Date;
import static android.arch.persistence.room.ForeignKey.CASCADE;

@Entity(
  tableName="flights",
  foreignKeys=@ForeignKey(
    entity=Trip.class,
    parentColumns="id",
    childColumns="tripId",
    onDelete=CASCADE),
  indices=@Index("tripId"))
@TypeConverters({TypeTransmogrifier.class})
class Flight extends Plan {
  public final String departingAirport;
  public final String arrivingAirport;
  public final String airlineCode;
  public final String flightNumber;
  public final String seatNumber;
  public final String tripId;

  @Ignore
  Flight(String title, int duration, Priority priority, Date startTime,
         String departingAirport, String arrivingAirport, String airlineCode,
         String flightNumber, String seatNumber, String tripId) {
    super(title, duration, priority, startTime);
    this.departingAirport=departingAirport;
    this.arrivingAirport=arrivingAirport;
    this.airlineCode=airlineCode;
    this.flightNumber=flightNumber;
    this.seatNumber=seatNumber;
    this.tripId=tripId;
  }

  Flight(String id, String title, int duration,
         Priority priority, Date startTime, Date creationTime,
         Date updateTime, String departingAirport, String arrivingAirport,
         String airlineCode, String flightNumber, String seatNumber,
         String tripId) {
    super(id, title, duration, priority, startTime, creationTime, updateTime);
    this.departingAirport=departingAirport;
    this.arrivingAirport=arrivingAirport;
    this.airlineCode=airlineCode;
    this.flightNumber=flightNumber;
    this.seatNumber=seatNumber;
    this.tripId=tripId;
  }
}

The Updated DAO and Database

Since we added new entities, TripDatabase needs to know about them, via the entities property on the @Database annotation:

package com.commonsware.android.room;

import android.arch.persistence.room.Database;
import android.arch.persistence.room.Room;
import android.arch.persistence.room.RoomDatabase;
import android.content.Context;

@Database(
  entities={Trip.class, Lodging.class, Flight.class},
  version=2
)
abstract class TripDatabase extends RoomDatabase {
  abstract TripStore tripStore();

  private static final String DB_NAME="trips.db";
  private static volatile TripDatabase INSTANCE=null;

  synchronized static TripDatabase get(Context ctxt) {
    if (INSTANCE==null) {
      INSTANCE=create(ctxt, false);
    }

    return(INSTANCE);
  }

  static TripDatabase create(Context ctxt, boolean memoryOnly) {
    RoomDatabase.Builder<TripDatabase> b;

    if (memoryOnly) {
      b=Room.inMemoryDatabaseBuilder(ctxt.getApplicationContext(),
        TripDatabase.class);
    }
    else {
      b=Room.databaseBuilder(ctxt.getApplicationContext(), TripDatabase.class,
        DB_NAME);
    }

    return(b.build());
  }
}

Note that now we are on version=2. Ideally, this sort of change would involve updating an existing database in-place, so as not to disturb any existing data. Room calls these “migrations”, and they are covered in an upcoming chapter.

TripStore, our DAO, now needs methods for Lodging and Flight as well:

package com.commonsware.android.room;

import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import java.util.List;

@Dao
interface TripStore {
  /*
    Trip
   */

  @Query("SELECT * FROM trips ORDER BY title")
  List<Trip> selectAllTrips();

  @Query("SELECT * FROM trips WHERE id=:id")
  Trip findTripById(String id);

  @Insert
  void insert(Trip... trips);

  @Update
  void update(Trip... trips);

  @Delete
  void delete(Trip... trips);

  /*
    Lodging
   */

  @Query("SELECT * FROM lodgings WHERE tripId=:tripId")
  List<Lodging> findLodgingsForTrip(String tripId);

  @Insert
  void insert(Lodging... lodgings);

  @Update
  void update(Lodging... lodgings);

  @Delete
  void delete(Lodging... lodgings);

  /*
    Flight
   */

  @Query("SELECT * FROM flights WHERE tripId=:tripId")
  List<Flight> findFlightsForTrip(String tripId);

  @Insert
  void insert(Flight... flights);

  @Update
  void update(Flight... flights);

  @Delete
  void delete(Flight... flights);
}

The Lodging and Flight @Query methods retrieve only those for a particular Trip, based on the ID. There is nothing stopping us from having other @Query methods (e.g., searching across all Lodging, regardless of Trip), but these will suffice for now.

We could elect to have separate DAO classes for each entity, or have nested @Dao-annotated classes inside the entity for these sorts of methods. In those cases, TripDatabase would have to be augmented with additional abstract methods to return instances of those classes, mirroring the existing tripStore() method.


Prev Table of Contents Next

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