M:N Relations in Room
For 1:1 relations, one entity has a foreign key back to the other entity.
For 1:N relations, one entity has a foreign key back to the other entity. In other words, 1:1 is simply 1:N for a specific small value of N.
In SQL, implementing M:N relations requires a join table of some form, where the join table has foreign keys back to the entities being related. Room, using SQL at its core, does not change this. And since Room does not model relations, but only foreign keys, to create an M:N relation, you have to create a “join entity” that winds up creating the associated join table.
In this chapter, we will take a look at how that is accomplished. Along the way, we will also look at other Room tidbits, such as how to use static classes as entities.
Implementing a Join Entity
The General/RoomMN
sample project demonstrates an M:N relation. From earlier chapters, we have a Customer
entity and a Category
entity. Previously, those were unrelated. Now, let’s implement an M:N relation between them, so a Customer
can be a member of zero or more categories, and a Category
can have zero or more customers.
Note that we are retaining the tree structure for Category
used previously. For the purposes of this chapter, we are ignoring that, considering a customer to belong to a category only via a direct relationship. So for example, if customer Foo
belongs to category Child
, which has a parent category Parent
, Foo
is not a member of Parent
. The tree structure simply organizes categories, without impacting customers.
Static Entity Classes
Much of the time, your entity classes will be standard, top-level Java classes. Sometimes, though, you might have some utility class that you would rather have as a static class, nested inside something else. For example, in the case of a join entity, perhaps you might want to tuck it inside of one of the entities being joined, just to reduce the clutter of your namespace.
Fortunately, this works, albeit with a wrinkle.
In the sample project, the Customer
class — which itself is an entity — has a static
class named CategoryJoin
that will serve as the join entity:
package com.commonsware.android.room.dao;
import android.arch.persistence.room.Embedded;
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.PrimaryKey;
import android.support.annotation.NonNull;
import java.util.Date;
import java.util.Set;
import java.util.UUID;
import static android.arch.persistence.room.ForeignKey.CASCADE;
@Entity(indices={@Index(value="postalCode", unique=true)})
class Customer {
@PrimaryKey
@NonNull
public final String id;
public final String postalCode;
public final String displayName;
public final Date creationDate;
@Embedded
public final LocationColumns officeLocation;
public final Set<String> tags;
@Ignore
Customer(String postalCode, String displayName, LocationColumns officeLocation,
Set<String> tags) {
this(UUID.randomUUID().toString(), postalCode, displayName, new Date(),
officeLocation, tags);
}
Customer(String id, String postalCode, String displayName, Date creationDate,
LocationColumns officeLocation, Set<String> tags) {
this.id=id;
this.postalCode=postalCode;
this.displayName=displayName;
this.creationDate=creationDate;
this.officeLocation=officeLocation;
this.tags=tags;
}
@Entity(
tableName="customer_category_join",
primaryKeys={"categoryId", "customerId"},
foreignKeys={
@ForeignKey(
entity=Category.class,
parentColumns="id",
childColumns="categoryId",
onDelete=CASCADE),
@ForeignKey(
entity=Customer.class,
parentColumns="id",
childColumns="customerId",
onDelete=CASCADE)},
indices={
@Index(value="categoryId"),
@Index(value="customerId")
}
)
public static class CategoryJoin {
@NonNull public final String categoryId;
@NonNull public final String customerId;
public CategoryJoin(String categoryId, String customerId) {
this.categoryId=categoryId;
this.customerId=customerId;
}
}
}
Room is perfectly content to work with this class, so long as you also register it with your RoomDatabase
via its @Database
annotation:
@Database(
entities={Customer.class, Category.class, Customer.CategoryJoin.class},
version=1
)
However, note that this is a static
class. Room will not be able to work with a non-static
nested class, as only instances of the outer class can create instances of the nested class.
Also, note that the default table name is based on the plain class name. In this case, the default table name is CategoryJoin
. The outer class name (Customer
) is not added into the table name. Normally, this will not be a problem, and you might be renaming the table anyway. However, where you can get tripped up is if you decided to have two (or more) classes with the same name, such as having CategoryJoin
inside both Customer
and some other entity. Then, you would wind up with two entity classes both trying to define the same table name by default, and Room will not like that very much.
Foreign Keys and Indices
Let’s take a closer look at the @Entity
annotation on Customer.CategoryJoin
:
@Entity(
tableName="customer_category_join",
primaryKeys={"categoryId", "customerId"},
foreignKeys={
@ForeignKey(
entity=Category.class,
parentColumns="id",
childColumns="categoryId",
onDelete=CASCADE),
@ForeignKey(
entity=Customer.class,
parentColumns="id",
childColumns="customerId",
onDelete=CASCADE)},
indices={
@Index(value="categoryId"),
@Index(value="customerId")
}
)
Here, we declare four properties.
tableName
renames the table to something that is more unique to this situation, incorporating both “customer” and “category” in the name. That way, if we do wind up with CategoryJoin
elsewhere, we can avoid table name collisions.
primaryKeys
is used, instead of @PrimaryKey
, because we need a composite key. The uniqueness is determined by the combination of the IDs of the Customer
and Category
, held in customerId
and categoryId
columns, respectively.
A join entity will need foreign keys back to both entities that it is joining. So, here, we have two @ForeignKey
annotations for the foreignKeys
property, connecting to both Customer
and Category
by their respective IDs. We also use onDelete=CASCADE
, so if the parent entity (Customer
or Category
) is deleted, we also delete all join entities associated with that parent.
And, since Room does not automatically add indices for foreign key columns, we add them ourselves, so we can rapidly find all of the join entity instances for a given Customer
or Category
.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.