Skip to main content
Version: 2.3.0

One-to-one

One-to-one (1:1) relationships represent a unique association between two entities, there is at most one model that can be connected on either side of the relation. This means we have to set a unique index on the foreign key in the database. Without the unique index the relation would be considered a one-to-many (1:n) relation.

Defining the Relationship

In the following examples we show how to configure a 1:1 relationship between User and Address.

With an id field

In the most simple case, all we have to do is add an id field on one of the models.

# address.yaml
class: Address
table: address
fields:
street: String

# user.yaml
class: User
table: user
fields:
addressId: int, relation(parent=address) // Foreign key field
indexes:
user_address_unique_idx:
fields: addressId
unique: true

In the example, the relation keyword annotates the addressId field to hold the foreign key. The field needs to be of type int and the relation keyword needs to specify the parent parameter. The parent parameter defines which table the relation is towards, in this case the Address table.

The addressId is required in this example because the field is not nullable. That means that each User must have a related Address. If you want to make the relation optional, change the datatype from int to int?.

When fetching a User from the database the addressId field will automatically be populated with the related Address object id.

With an object

While the previous example highlights manual handling of data, there's an alternative approach that simplifies data access using automated handling. By directly specifying the Address type in the User class, Serverpod can automatically handle the relation for you.

# address.yaml
class: Address
table: address
fields:
street: String

# user.yaml
class: User
table: user
fields:
address: Address?, relation // Object relation field
indexes:
user_address_unique_idx:
fields: addressId
unique: true

In this example, we define an object relation field by annotating the address field with the relation keyword where the type is another model, Address?.

Serverpod then automatically generates a foreign key field (as seen in the last example) named addressId in the User class. This auto-generated field is non-nullable by default and is by default always named from the object relation field with the suffix Id.

The object field, in this case address, must always be nullable (as indicated by Address?).

An object relation field gives a big advantage when fetching data. Utilizing relational queries enables filtering based on relation attributes or optionally including the related data in the result.

No parent keyword is needed here because the relational table is inferred from the type on the field.

Optional relation

# user.yaml
class: User
table: user
fields:
address: Address?, relation(optional)
indexes:
user_address_unique_idx:
fields: addressId
unique: true

With the introduction of the optional keyword in the relation, the automatically generated addressId field becomes nullable. This means that the addressId can either hold a foreign key to the related address table or be set to null, indicating no associated address.

Custom foreign key field

Serverpod also provides a way to customize the name of the foreign key field used in an object relation.

# user.yaml
class: User
table: user
fields:
customIdField: int
address: Address?, relation(field=customIdField)
indexes:
user_address_unique_idx:
fields: customIdField
unique: true

In this example, we define a custom foreign key field with the field parameter. The argument defines what field that is used as the foreign key field. In this case, customIdField is used instead of the default auto-generated name.

If you want the custom foreign key to be nullable, simply define its type as int?. Note that the field keyword cannot be used in conjunction with the optional keyword. Instead, directly mark the field as nullable.

Generated SQL

The following code block shows how to set up the same relation with raw SQL. Serverpod will generate this code behind the scenes.

CREATE TABLE "address" (
"id" serial PRIMARY KEY,
"street" text NOT NULL
);

CREATE TABLE "user" (
"id" serial PRIMARY KEY,
"addressId" integer NOT NULL
);


CREATE UNIQUE INDEX "user_address_unique_idx" ON "user" USING btree ("addressId");

ALTER TABLE ONLY "user"
ADD CONSTRAINT "user_fk_0"
FOREIGN KEY("addressId")
REFERENCES "address"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;

Independent relations defined on both sides

You are able to define as many independent relations as you wish on each side of the relation. This is useful when you want to have multiple relations between two entities.

# user.yaml
class: User
table: user
fields:
friendsAddress: Address?, relation
indexes:
user_address_unique_idx:
fields: friendsAddressId
unique: true

# address.yaml
class: Address
table: address
fields:
street: String
resident: User?, relation
indexes:
address_user_unique_idx:
fields: residentId
unique: true

Both relations operate independently of each other, resulting in two distinct relationships with their respective unique indexes.

Bidirectional relations

If access to the same relation is desired from both sided, a bidirectional relation can be defined.

# user.yaml
class: User
table: user
fields:
addressId: int
address: Address?, relation(name=user_address, field=addressId)
indexes:
user_address_unique_idx:
fields: addressId
unique: true

# address.yaml
class: Address
table: address
fields:
street: String
user: User?, relation(name=user_address)

The example illustrates a 1:1 relationship between User and Address where both sides of the relationship are explicitly specified.

Using the name parameter, we define a shared name for the relationship. It serves as the bridge connecting the address field in the User class to the user field in the Address class. Meaning that the same User referencing an Address is accessible from the Address as well.

Without specifying the name parameter, you'd end up with two unrelated relationships.

When the relationship is defined on both sides, it's required to specify the field keyword. This is because Serverpod cannot automatically determine which side should hold the foreign key field. You decide which side is most logical for your data.

In a relationship where there is an object on both sides a unique index is always required on the foreign key field.