Relations
Relations are declared with four decorators on the entity class. mappedBy takes either a property name or a callback, the callback surviving a rename.
import { v7 as uuidv7 } from 'uuid';import { Entity, Id, Field, OneToOne, OneToMany, ManyToOne, ManyToMany,} from 'uql-orm';
@Entity()export class User { @Id({ type: 'uuid', onInsert: uuidv7, }) id?: string;
@Field({ type: String }) name?: string;
/** * One-to-One: A user has one profile. * mappedBy can be a callback for better refactoring support. */ @OneToOne({ entity: () => Profile, mappedBy: (profile) => profile.user, cascade: true, }) profile?: Profile;
/** * One-to-Many: A user can have many posts. */ @OneToMany({ entity: () => Post, mappedBy: (post) => post.author, }) posts?: Post[];}
@Entity()export class Profile { @Id({ type: 'uuid', onInsert: uuidv7, }) id?: string;
@Field({ type: String }) picture?: string;
/** * Foreign key column. The 'references' option points at the target entity; * the column type is inherited from the target's primary key. */ @Field({ references: () => User }) userId?: string;
@OneToOne({ entity: () => User }) user?: User;}
@Entity()export class Post { @Id({ type: Number }) id?: number;
@Field({ type: String }) title?: string;
@Field({ references: () => User }) authorId?: string;
@ManyToOne({ entity: () => User }) author?: User;
/** * Many-to-Many: A post can have many tags. * 'through' specifies the pivot entity. */ @ManyToMany({ entity: () => Tag, through: () => PostTag, cascade: true, }) tags?: Tag[];}
@Entity()export class Tag { @Id({ type: 'uuid', onInsert: uuidv7, }) id?: string;
@Field({ type: String }) name?: string;}
@Entity()export class PostTag { @Id({ type: 'uuid', onInsert: uuidv7, }) id?: string;
@Field({ references: () => Post }) postId?: number;
@Field({ references: () => Tag }) tagId?: string;}How a to-many says where its rows are
Section titled “How a to-many says where its rows are”A to-one relation carries its own foreign key, so nothing more is needed. A to-many has no such column and needs one of three, which the compiler requires and the entity re-checks when it is first resolved:
| Option | Use |
|---|---|
mappedBy |
The inverse side: names the field on the other entity that holds the foreign key or relation. |
through |
A junction entity with a foreign key to each side. Works for @ManyToMany and @OneToMany alike. |
references |
The join columns, by name, when neither convention fits. |
With through, both join columns are read from the junction and named after the two entities (postId, tagId above). A junction missing one of them is also reported when the entity is resolved, rather than at the first query.
Querying Relations
Section titled “Querying Relations”Scalar columns go in $select, related entities in $populate, each with its own selection and filter:
const posts = await pool.findMany(Post, { $select: { id: true, title: true }, $populate: { author: { $select: { id: true, name: true }, }, tags: { $select: { name: true }, $where: { name: { $istartsWith: 'typescript' } }, }, }, $where: { author: { name: 'Roger' }, },});-- Main query with LEFT JOIN for ManyToOneSELECT "Post"."id", "Post"."title", "author"."id" "author.id", "author"."name" "author.name"FROM "Post"LEFT JOIN "User" "author" ON "author"."id" = "Post"."authorId"WHERE EXISTS ( SELECT 1 FROM "User" WHERE "User"."id" = "Post"."authorId" AND "User"."name" = $1)-- ManyToMany tags loaded via a second query: it reads the junction and joins the-- target, so the tag's own $where lands on the join, and every parent's ids arrive-- as one array parameter rather than an expanded IN list.SELECT "PostTag"."postId", "tag"."id" "tag.id", "tag"."name" "tag.name"FROM "PostTag"INNER JOIN "Tag" "tag" ON "tag"."id" = "PostTag"."tagId" AND "tag"."name" ILIKE $1WHERE "PostTag"."postId" = ANY($2)Check the querying relations section for more advanced examples on deep filtering and selection. Against a database that already has tables, these decorators are written for you: generate:from-db infers them from the foreign keys it finds.