include loads associated rows alongside the parents. This guide is about theprorm.define() sends its statements with the values alreadyimport { Prorm, DataTypes } from 'ts-prorm-orm';
export const prorm = new Prorm({ dialect: 'postgres', host: 'localhost', database: 'shop' });
const pk = { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true };
export const User = prorm.define('User', {
id: pk,
name: { type: DataTypes.STRING(100) },
email: { type: DataTypes.STRING(255) },
status: { type: DataTypes.STRING(20) },
}, { tableName: 'users', timestamps: false });
export const Order = prorm.define('Order', {
id: pk,
userId: { type: DataTypes.INTEGER() },
status: { type: DataTypes.STRING(20) },
total: { type: DataTypes.DECIMAL(10, 2) },
}, { tableName: 'orders', timestamps: false });
export const OrderItem = prorm.define('OrderItem', {
id: pk,
orderId: { type: DataTypes.INTEGER() },
productId: { type: DataTypes.INTEGER() },
quantity: { type: DataTypes.INTEGER() },
}, { tableName: 'order_items', timestamps: false });
export const Product = prorm.define('Product', {
id: pk,
sku: { type: DataTypes.STRING(32) },
name: { type: DataTypes.STRING(200) },
price: { type: DataTypes.DECIMAL(10, 2) },
}, { tableName: 'products', timestamps: false });
export const Tag = prorm.define('Tag', {
id: pk,
name: { type: DataTypes.STRING(50) },
}, { tableName: 'tags', timestamps: false });
export const ProductTag = prorm.define('ProductTag', {
productId: { type: DataTypes.INTEGER() },
tagId: { type: DataTypes.INTEGER() },
}, { tableName: 'product_tags', timestamps: false });
User.hasMany(Order, { foreignKey: 'userId', as: 'orders' });
Order.belongsTo(User, { foreignKey: 'userId', as: 'customer' });
Order.hasMany(OrderItem, { foreignKey: 'orderId', as: 'items' });
OrderItem.belongsTo(Product, { foreignKey: 'productId', as: 'product' });
Product.belongsToMany(Tag, { through: { model: ProductTag }, foreignKey: 'productId', as: 'tags' });include with one additional query per association level,SELECTconst customers = await User.findAll({ include: [{ model: Order, as: 'orders' }] });
customers.map((u) => [u.name, u.orders.length]); // [['Ada', 2], ['Grace', 1], ['Linus', 0]]LIMIT 20 on the parent means twenty$alias.column$ filtering plus anorder, or drop to raw SQL.const customers = await User.findAll({
include: [{
model: Order,
as: 'orders',
include: [{ model: OrderItem, as: 'items', include: [{ model: Product, as: 'product' }] }],
}],
});
customers[0].orders[0].items.map((i) => i.product.name); // ['Keyboard', 'Mouse']LIMIT 20 on the parent still means twenty users.[!NOTE]
Key lists are not de-duplicated before they are sent:IN (1,2,2,1)above has
one entry per order item. The database treats the duplicates as one value, so
the result is right; the list just grows with the child rows.
dataValues:| Association | Attached value |
|---|---|
belongsTo, hasOne | a single instance, or null |
hasMany, belongsToMany | an array (empty when there are no children) |
const customers = await User.findAll({ include: [{ model: Order, as: 'orders' }] });
customers[0].orders; // Order[]
const placed = await Order.findAll({ include: [{ model: User, as: 'customer' }] });
placed[0].customer; // User | nullawait Order.findAll({ include: [{ model: User, as: 'customer' }] });await User.findAll({ include: [{ model: Order, as: 'orders' }] });const products = await Product.findAll({ include: [{ model: Tag, as: 'tags' }] });
products.map((p) => [p.name, p.tags.map((t) => t.name)]);
// [['Keyboard', ['peripherals', 'sale']], ['Mouse', ['peripherals']]]where, or marked required: true, means inner joinLIMIT, Prorm resolves the matching parent keysconst users = await User.findAll({
include: [{ model: Order, as: 'orders', where: { status: 'pending' } }],
});
users.map((u) => [u.name, u.orders.length]); // [['Ada', 1]]required: true does the same thing without a predicate: "only users thatfindAll short-circuits and[] without running the main query:await User.findAll({ include: [{ model: Order, as: 'orders', where: { status: 'refunded' } }] });
// -> SELECT * FROM "orders" WHERE "status" = 'refunded' (and nothing else)limit applies after the filter, so it counts qualifying parents:await User.findAll({ limit: 1, include: [{ model: Order, as: 'orders', where: { status: 'paid' } }] });[!WARNING]
Step 1 is not restricted to any parents: it reads every child row that
matches the include'swhere. Withrequired: trueand nowhere, it isSELECT * FROM "orders", the whole child table. On a large child table,
give the include a selectivewhere, or query the child model first and pass
the parent ids yourself.
antiJoin: true (also spelled exclude: true or joinType: 'anti') invertsconst neverOrdered = await User.findAll({
include: [{ model: Order, as: 'orders', antiJoin: true }],
});
neverOrdered.map((u) => u.name); // ['Linus']antiJoinType ('notExists' | 'notIn' | 'leftJoinIsNull') is accepted andNOT IN.joinType: 'semi' filters the parents like required: true — only parents$alias.column$where key of the form $alias.column$ names a column on an includedawait User.findAll({
include: [{ model: Order, as: 'orders' }],
where: { '$orders.status$': 'paid' },
});AssociationError telling you toAssociationError: where key '$items.status$' refers to 'items', which is not included
in this query. Add `include: [{ model: ..., as: 'items' }]`.| Option | Honoured | Effect |
|---|---|---|
model | yes | The model to load. Required — an entry without one is skipped. |
as | yes | Which association to use when several link the same two models. |
where | yes | Filters the children and restricts the parents (see above). |
attributes | yes | Column list for the child query. |
order | yes | ORDER BY on the child query. |
required | yes | Inner-join semantics. |
antiJoin / exclude / joinType: 'anti' | yes | Keep only parents with no match (see above). |
include | yes | Nested includes, to any depth. |
limit / offset | yes | A window per parent, not across the whole result — see below. |
{ all: true } | yes | Expands to one include per association. { all: 'alias' } and { all: true, nested: true } too. |
separate | n/a | Every include is already a separate query. |
await User.findAll({
include: [{
model: Order,
as: 'orders',
attributes: ['id', 'total'],
order: [['total', 'DESC']],
}],
});limit / offset are a per-parent windowawait User.findAll({
include: [{ model: Order, as: 'orders', order: [['id', 'DESC']], limit: 3 }],
});LIMIT cannot simply go into thatLIMIT 3 there would return three rows in total and leave everyORDER BY (the[!WARNING]
That is correct, but it does not reduce what crosses the wire: every
matching child is still fetched and then sliced in memory. For "the latest
three orders" of ten thousand users with long histories, query the child
model directly instead.
findByPk/findOnehasMany only:const ada = await User.findByPk(1, {
include: [{ model: Order, as: 'orders', order: [['id', 'DESC']], limit: 3 }],
});belongsToMany never pushes down: the junction means fetched rows and attachedLIMIT would count the wrong thing.belongsTo / hasOne) a window is meaningless; it islimit/offset that isAssociationError instead of being coerced.{ all: true }await Order.findAll({ include: [{ all: true }] }); // every association
await User.findAll({ include: [{ all: true, nested: true }] }); // and theirs, recursivelyOrder has two associations, so { all: true } is two include queries:User ⇄ Order graph settles instead of growing{ all: 'alias' } throws.[!CAUTION] { all: true, nested: true }is convenient in a REPL and expensive in a
request. On this small schema oneUser.findAll()already runs seven
statements: every association of every reachable model, including the way
back tousersthroughOrder.customer. Name the includes you need.
{ all: true } composes with required: true — expansion now happens beforeawait User.findAll({ include: [{ all: true, required: true }] });
// -> SELECT * FROM "users" WHERE "id" IN (…) — only parents with matchesscope declared on the association is a fixed condition applied every timewhere wins where the twoUser.hasMany(Order, { foreignKey: 'userId', as: 'paidOrders', scope: { status: 'paid' } });
const users = await User.findAll({ include: [{ model: Order, as: 'paidOrders' }] });
users.map((u) => [u.name, u.paidOrders.length]); // [['Ada', 1], ['Grace', 1], ['Linus', 0]]User and Order, an include without as isAssociationError: User has 2 associations to Order (orders, paidOrders).
Disambiguate with `include: [{ model: Order, as: 'orders' }]`.withCount avoids buildingconst users = await User.findAll({ withCount: ['orders'] });
users.map((u) => [u.name, u.ordersCount]); // [['Ada', 2], ['Grace', 1], ['Linus', 0]][!TIP]
The rows are tallied in JavaScript, so this saves instance construction and
round trips but still transfers the child rows. For a large child table, an
aggregate query against the child model is cheaper.
belongsTo counts are resolved without any query at all — the answer is 0 or 1const user = await User.findByPk(1);
const orders = await user.getOrders(); // findAll on Order, scoped to this user
const n = await user.countOrders();
await user.addOrder(order); // link an existing row
await user.createOrder({ status: 'pending' }); // insert one already linked[!NOTE] src/models/model.tsalso defines genericgetRelated(alias)/setRelated/createRelated/hasRelatedhelpers, but they live on the unreachablecreateModel()factory (see Streaming),
not on the instancesdefine()/addModel()produce. Use the generated
accessors above.
include when you have many parents; reach for the accessors when| Error | Cause |
|---|---|
AssociationError: X has no association to Y | The association was never declared, or is declared in the other direction. |
AssociationError: X has N associations to Y (…) | Ambiguous — pass as to pick one. |
AssociationError: … has no association to Y aliased 'z' | The as does not match any association's effective alias. |
AssociationError: where key '$z.col$' refers to 'z', which is not included … | A $alias.column$ key names an alias you did not include. |
EagerLoadError | The child query itself failed; the cause is attached. |