Associations
Read this page in the documentation
Associations Associations connect two models by a foreign key and generate a set of instance helper methods (getX, setX, addX, createX, removeX, countX, hasX) on the source model. prorm supports four association types — hasOne, hasMany, belongsTo, and belongsToMany — declared by calling the matching method on a model returned from prorm.define(...). Every association is registered on the source model so that later helpers, eager loading (include), and foreign-key constraint generation during sync() can all resolve it. This guide covers each type, the generated mixin methods, junction tables for many-to-many, eager loading, and how foreign keys are inferred and constrained. The four shapes Which side holds the foreign key is the thing to get right, because it decides which table is written when you set the association: Association | Foreign key lives on | Extra table | --- | --- | --- | hasOne | the target | — | hasMany | the target | — | belongsTo | the source | — | belongsToMany | neither | a join table | hasOne and belongsTo describe the same physical relationship from opposite ends — declare both when you want traversal in both directions. Defining associations Associations are defined after the models exist. Each call takes the target model and an options object: The options accepted by every association type are: Option | Purpose | as | Alias for the association; drives the helper method names and the include key. | foreignKey | Foreign key column name. A string, an array (composite key), or { name, allowNull }. | sourceKey | Key on the source model. Defaults to the primary key. | targetKey | Key on the target model. Defaults to id. | onDelete / onUpdate | Referential action emitted in the FOREIGN KEY constraint (CASCADE, SET NULL, RESTRICT, NO ACTION, SET DEFAULT). | constraints | Whether to emit the foreign-key constraint. | through | For belongsToMany only: the junction table name, model, or { model, otherKey, ... }. | Alias and foreign-key defaults When you omit as, the alias is derived from the target model name: hasOne / belongsTo use the lower-cased target name (User -> user). hasMany / belongsToMany append s (Post -> posts). When you omit foreignKey, it is inferred as a lowerCamelCase column name: belongsTo and hasOne: <targetName>Id (e.g. Post.belongsTo(User) -> userId). hasMany and belongsToMany: <sourceName>Id (e.g. User.hasMany(Post) -> userId). Because the inferred name is always lowerCamelCase, define the matching column with the same casing (userId, not UserId). For hasOne the foreign key lives on the target table, so an explicit foreignKey is often clearer than relying on the default. belongsTo belongsTo places the foreign key on the source model. The source row stores the target's primary key. Generated methods (alias Author): getAuthor, setAuthor, createAuthor, hasAuthor, countAuthor. setAuthor accepts an instance, a raw key value, or null, then saves the source row unless you pass { save: false }. hasOne hasOne is the inverse one-to-one: the foreign key lives on the target model, pointing back at the source. Generated methods (alias Profile): getProfile, setProfile, createProfile, hasProfile, countProfile. hasMany hasMany is one-to-many. The foreign key lives on the target, and the helpers operate on collections. Generated methods (alias Posts): getPosts, setPosts, addPosts, createPosts, removePosts, countPosts, hasPosts. Singular aliases are derived by stripping a trailing s (or ies -> y): addPost, removePost, hasPost, createPost. setPosts destroys the existing linked rows and recreates them; addPosts/removePosts only flip the foreign key on the given rows. belongsToMany belongsToMany is many-to-many, backed by a junction (through) table that holds one row per link. The source's key is the foreignKey; the target's key on the junction is the otherKey. Generated methods (alias Roles): getRoles, setRoles, addRoles, removeRoles, createRoles, countRoles, hasRoles, plus the singular aliases (addRole, removeRole, hasRole, createRole) and addRolesModel / removeRolesModel variants. addRoles uses findOrCreate on the junction, so linking the same pair twice is a no-op. The through table through accepts three shapes: When no through model is supplied, prorm synthesizes one whose table name defaults to <Source><Target> (e.g. UserRole) with two non-null integer columns — the foreignKey referencing the source's id and the otherKey referencing the target's id. The otherKey defaults to <targetName>Id (e.g. roleId) and can be set with the top-level otherKey option or through.otherKey. Eager loading with include Instead of calling getX() per row, pass include to findAll / findOne / findByPk to load associated rows in one pass. Each associated result is attached under the association alias (and mirrored on dataValues): a single object (or null) for belongsTo/hasOne, an array for hasMany/belongsToMany. Use as when a model is included under a custom alias, and where, attributes, and order to shape the associated rows: where, attributes, and order are applied per-included association. Top-level limit / offset still page the parent query. Nested includes Includes nest to any depth. Each level's fetched rows become the parent for the next, so grandchildren load too: Including belongsToMany Many-to-many includes resolve through the junction table automatically using the association's foreignKey and otherKey. You can still narrow the joined target rows with where, attributes, and order: Foreign keys and constraints During sync(), each association contributes a FOREIGN KEY constraint on the table that holds the key. The referenced column is the targetKey (default id), and onDelete / onUpdate are emitted when set: This produces a constraint of the form: Composite foreign keys are supported by passing an array to foreignKey (and, if needed, targetKey); the constraint is rendered as a parenthesized column list: The object form of foreignKey carries constraint metadata such as allowNull: Custom primary keys The helpers resolve the source instance's key from the model's declared primary key attribute — it does not have to be named id. A model keyed on sku works the same way; the association reads whichever attribute is marked primaryKey (falling back to id/uuid when none is declared). Next: Eager loading — fetching related rows without the N+1 problem. Related reading Core concepts — the reading path these belong to Going further — the specialised material