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:
- Sets up the relation as being with
Trip
(entity=Trip.class
) - Ties the
id
column onTrip
(parentColumns="id"
) to thetripId
onLodging
(childColumns="tripId"
) - Indicates that if the
Trip
is deleted, associatedLodging
instances should also be deleted (onDelete=CASCADE
)
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.