DEV Community

Cover image for Working with circular dependencies in sequelize-typescript
Ashutosh Sahu
Ashutosh Sahu

Posted on

Working with circular dependencies in sequelize-typescript

When designing relational databases, it's pretty common to run into tables that just can't stop referencing each other. These circular references can be a bit of a headache, especially when using an ORM like Sequelize. Whether you're dealing with tables that reference themselves or ones that are interdependent, knowing how to manage these circular references is crucial for maintaining a clean and functional database schema.

In this article, we'll explore what circular references are, why they happen, and how to effectively handle them in Sequelize-typescript.

What Are Circular References?

Circular references happen when two or more tables reference each other, creating a loop. For example:

  • Table A has a foreign key pointing to Table B.
  • Table B has a foreign key pointing back to Table A.

This loop can complicate database operations like inserts, updates, and deletions.

Common Scenarios for Circular References

  1. Self-Referencing Tables: A table references itself, like an Employee table where each employee has a manager_id referencing another employee in the same table.
  2. Interdependent Tables: Two or more tables reference each other, like a User table and a Team table where a user belongs to a team, and a team has a leader who is a user.

In these cases, making the foreign keys nullable can help manage the relationships better.

Got it! Let's make this straightforward and clear.


The Problem

When I started writing models for these circular references in sequelize-typescript, I ran into an issue right away. Thanks to linter which helped identify issue earlier that there is a circular dependency problem in the imports.

For example, I had two models: User and Team.

// user.model.ts
import { Team } from './team.model';

export class User extends Model {
  ...
  @ForeignKey(() => Team)
  @Column({ type: DataTypes.INTEGER, allowNull: true })
  teamId?: string;

  @BelongsTo(() => Team)
  team?: Team;
}
Enter fullscreen mode Exit fullscreen mode
// team.model.ts
import { User } from './user.model';

export class Team extends Model { 
  ...
  @ForeignKey(() => User)
  @Column({ type: DataTypes.INTEGER, allowNull: true })
  leaderId?: string;

  @HasOne(() => User)
  leader?: User;
}
Enter fullscreen mode Exit fullscreen mode

See the problem? The User model needs to import the Team model, and the Team model needs to import the User model. This circular dependency in imports is causing issues and preventing progress.

### The Search for a Solution

I searched high and low for a way to resolve this dependency problem, but there was no direct solution that fit my needs. Every answer on Stack Overflow seemed to say the same thing: avoid circular dependencies altogether. But my situation was unique.

Then, I stumbled upon a suggestion to introduce a middleman to break the circular dependencies. For example, if A depends on B and B depends on A, you can introduce C so that A depends on C and B depends on C.

But how could I split my models any further? 🤔 This was the challenge I needed to tackle.

Inheritance to the Rescue

I found a solution using inheritance. Models typically contain two types of data: actual database columns and associations. So, what if I create a separate class for associations that extends the model containing the table columns?

One limitation with this approach is that it doesn't support deep nested joins in code. However, I see this more as a best practice than a limitation. When defining associations, I only need access to the columns of associated tables, not their associations.

For example, if I need to access the name of the team leader for a given user, Sequelize allows me to use user.team.leader.name. But I'm opting out of this convenience. Instead, I'll get the user.team.leaderId and then fetch the user in a second query using the leaderId.

This approach solves most of the problem, but I still needed the import for defining @ForeignKey. Fortunately, I discovered that the decorator isn't necessary if you provide the keys directly to the association decorators. Here's what my solution looks like:

// user.table.ts
@Table({ tableName: 'user', underscored: true })
export class UserTable extends Model {
    ...
    @Column({ type: DataTypes.INTEGER, allowNull: true })
    teamId?: string;
}
Enter fullscreen mode Exit fullscreen mode
// user.model.ts
import { TeamTable } from './team.table.ts'

export class User extends UserTable {
    ...
    @BelongsTo(() => Team, { foreignKey: 'teamId', targetKey: 'id' })
    team?: TeamTable
}

Enter fullscreen mode Exit fullscreen mode
// team.table.ts

@Table({ tableName: 'team', underscored: true })
export class TeamTable extends Model {
    ...
    @Column({ type: DataTypes.INTEGER, allowNull: true })
    leaderId?: string;
}
Enter fullscreen mode Exit fullscreen mode
// team.model.ts

import { UserTable } from './user.table.ts'

export class Team extends TeamTable {
    ...
    @HasOne(() => User, { sourceKey: 'leaderId', foreignKey: 'id'})
    leader?: UserTable
}

Enter fullscreen mode Exit fullscreen mode

This way, I can manage circular dependencies without running into import issues.

SurveyJS custom survey software

JavaScript UI Libraries for Surveys and Forms

SurveyJS lets you build a JSON-based form management system that integrates with any backend, giving you full control over your data and no user limits. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more.

Learn more

Top comments (0)

Billboard image

Try REST API Generation for Snowflake

DevOps for Private APIs. Automate the building, securing, and documenting of internal/private REST APIs with built-in enterprise security on bare-metal, VMs, or containers.

  • Auto-generated live APIs mapped from Snowflake database schema
  • Interactive Swagger API documentation
  • Scripting engine to customize your API
  • Built-in role-based access control

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay