Relations
Give cats owners: a one-to-many and its many-to-one, typed with Relation<> so the two entity files can import each other under ES modules, loaded on request and set by assignment.
What you'll learn
- Declare a one-to-many relation and its inverse many-to-one, and say which side holds the foreign key
- Explain why a relation property typed with the bare class can fail at load time under ES modules, and type it with Relation<>
- Load a relation on request with relations: { ... }, and set one by assigning the entity and saving
A cat has an owner, an owner has cats. In an array that is a nested object; in a database it is two tables and a foreign key, cat.ownerId pointing at owner.id. TypeORM lets you keep writing cat.owner and owner.cats and works out the key, the join and the direction, provided you tell it which properties are related and how. This lesson adds owners to the cats API and, along the way, meets the one place where the docs' example fails under native ES modules and what TypeORM offers instead.
Three kinds of relation
TypeORM has @OneToOne(), @OneToMany() with its counterpart @ManyToOne(), and @ManyToMany(). The docs' example is the one-to-many, a user with photos:
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@OneToMany((type) => Photo, (photo) => photo.user)
photos: Photo[];
}
Two arrows. The first, () => Photo, names the other entity lazily, as a function, because when this class is being defined Photo may not be defined yet: the two files import each other. The second, (photo) => photo.user, names the inverse side, the property on Photo that points back. The other side looks like this:
@Entity()
export class Photo {
@ManyToOne(() => User, (user) => user.photos)
user: User;
}
The @ManyToOne side owns the foreign key: a userId column appears on the photo table, and nothing appears on user. Options on the decorator control the rest: { nullable: true } allows a photo with no user, { onDelete: 'CASCADE' } deletes photos with their user, { eager: true } loads the relation with every query, { cascade: true } saves related objects when the owner is saved.
Loading a relation
By default a relation is not loaded: find() returns cats whose owner property is simply absent, because joining every table every time would be slow. Ask for it:
this.cats.find({ relations: { owner: true } });
this.owners.findOne({ where: { id }, relations: { cats: true } });
A loaded relation with no row behind it is null. Setting one is assigning the entity: cat.owner = owner; await this.cats.save(cat); writes the foreign key; cat.owner = null clears it.
The ESM trap, and Relation<>
Here is the docs' user: User under native ES modules, which is what this runtime and a Nest 12 project run. TypeScript emits the property's type as decorator metadata, design:type, evaluated when the class is defined. cat.entity.ts imports owner.entity.ts and vice versa, so whichever file loads second reads the other's class before its initialisation, and the boot dies with Cannot access 'Owner' before initialization. Which file loads second depends on who imported what first, so the bug appears and disappears with unrelated edits. Fundamentals lesson 6 met the same error on a constructor parameter.
TypeORM's own answer is the Relation<T> wrapper type:
@ManyToOne(() => Owner, (owner) => owner.cats)
owner: Relation<Owner>;
@OneToMany(() => Cat, (cat) => cat.owner)
cats: Relation<Cat[]>;
Relation<Owner> is Owner to the type checker, so nothing changes in your code, and it is a type alias, so the emitted metadata is Object and never touches the class at load time. The lazy () => Owner in the decorator is evaluated later, when TypeORM builds its metadata, by which time both files are loaded. Use Relation<> on every relation property, both sides.
Your task
Cats gain owners.
owner.entity.ts: an owner has many cats, the inverse ofCat.owner, typed withRelation<>.cat.entity.ts: a cat belongs to at most one owner, nullable, inverseOwner.cats, typed withRelation<>.CatsService:findAllandfindOneload the owner;findOwnerloads the cats;adoptfinds the owner (404"Owner #9 not found" when missing), assigns it, saves, and returns the cat with its owner loaded.
Try step 2 with the docs' bare owner: Owner first and run. Depending on which file loads first you may see the boot fail with the before initialization error, and now you know why.
When it fails
Cannot access 'Owner' before initialization: a relation property is typed with the bare class. UseRelation<Owner>.owneris missing from every cat: the relation exists but was not loaded; addrelations: { owner: true }.Entity "Owner" ... relation "cats" ... inverse sideor similar metadata errors: the second arrow points at a property that does not exist on the other class, or is missing on one side.POST /cats/1/adopt/1answers201withowner: null: the owner was assigned but the response came from the object before the save, orfindOneafterwards did not load the relation.
Remember
@OneToManyand@ManyToOnecome in pairs; the many-to-one side holds the foreign key.- Both arrows are lazy: the entity, then the inverse property.
- Relations load only when asked, with
relations: { name: true }; unloaded is absent, loaded-but-empty isnull. - Under ES modules, type relation properties with
Relation<T>; the bare class breaks depending on load order.
Stuck? Show a hint
owner.entity.ts: @OneToMany(() => Cat, (cat) => cat.owner) cats: Relation<Cat[]>. cat.entity.ts: @ManyToOne(() => Owner, (owner) => owner.cats, { nullable: true }) owner: Relation<Owner> | null. Service: find({ relations: { owner: true }, order: { id: 'ASC' } }); findOne with the same relations; in adopt, findOneBy the owner, throw NotFoundException when null, cat.owner = owner, save, then return findOne(id); findOwner with relations: { cats: true }.