JOIN Table Migrations in Sequelize

Think of JOIN tables as bridge builders in your database - they create connections between different entities that need to have many-to-many relationships. Let's learn how to construct these bridges effectively!

Understanding JOIN Tables

Imagine you're organizing a university course system. Students can take multiple courses, and courses can have multiple students. The JOIN table is like the course registration system that connects them:


// First, create the main tables
// Students table migration
module.exports = {
    up: async (queryInterface, Sequelize) => {
        await queryInterface.createTable('Students', {
            id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER
            },
            name: {
                type: Sequelize.STRING,
                allowNull: false
            },
            email: {
                type: Sequelize.STRING,
                unique: true
            },
            createdAt: {
                allowNull: false,
                type: Sequelize.DATE,
                defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
            }
        });
    }
};

// Courses table migration
module.exports = {
    up: async (queryInterface, Sequelize) => {
        await queryInterface.createTable('Courses', {
            id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER
            },
            title: {
                type: Sequelize.STRING,
                allowNull: false
            },
            code: {
                type: Sequelize.STRING,
                unique: true
            },
            createdAt: {
                allowNull: false,
                type: Sequelize.DATE,
                defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
            }
        });
    }
};

// Now, create the JOIN table
module.exports = {
    up: async (queryInterface, Sequelize) => {
        await queryInterface.createTable('StudentCourses', {
            id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER
            },
            studentId: {
                type: Sequelize.INTEGER,
                allowNull: false,
                references: {
                    model: 'Students',
                    key: 'id'
                },
                onDelete: 'CASCADE'
            },
            courseId: {
                type: Sequelize.INTEGER,
                allowNull: false,
                references: {
                    model: 'Courses',
                    key: 'id'
                },
                onDelete: 'CASCADE'
            },
            enrollmentDate: {
                type: Sequelize.DATE,
                defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
            }
        });
    }
};
                

Real-World Examples

E-commerce Product Tags System


// Products table
module.exports = {
    up: async (queryInterface, Sequelize) => {
        await queryInterface.createTable('Products', {
            id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER
            },
            name: {
                type: Sequelize.STRING,
                allowNull: false
            },
            price: {
                type: Sequelize.DECIMAL(10, 2),
                allowNull: false
            }
        });

        await queryInterface.createTable('Tags', {
            id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER
            },
            name: {
                type: Sequelize.STRING,
                allowNull: false,
                unique: true
            }
        });

        // JOIN table
        await queryInterface.createTable('ProductTags', {
            productId: {
                type: Sequelize.INTEGER,
                references: {
                    model: 'Products',
                    key: 'id'
                },
                onDelete: 'CASCADE'
            },
            tagId: {
                type: Sequelize.INTEGER,
                references: {
                    model: 'Tags',
                    key: 'id'
                },
                onDelete: 'CASCADE'
            },
            createdAt: {
                allowNull: false,
                type: Sequelize.DATE,
                defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
            }
        });
    }
};
                

Social Media Skills and Users


module.exports = {
    up: async (queryInterface, Sequelize) => {
        // Skills table
        await queryInterface.createTable('Skills', {
            id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER
            },
            name: {
                type: Sequelize.STRING,
                allowNull: false,
                unique: true
            },
            category: {
                type: Sequelize.STRING
            }
        });

        // UserSkills JOIN table
        await queryInterface.createTable('UserSkills', {
            userId: {
                type: Sequelize.INTEGER,
                references: {
                    model: 'Users',
                    key: 'id'
                },
                onDelete: 'CASCADE'
            },
            skillId: {
                type: Sequelize.INTEGER,
                references: {
                    model: 'Skills',
                    key: 'id'
                },
                onDelete: 'CASCADE'
            },
            proficiencyLevel: {
                type: Sequelize.ENUM('beginner', 'intermediate', 'expert'),
                defaultValue: 'beginner'
            },
            endorsements: {
                type: Sequelize.INTEGER,
                defaultValue: 0
            }
        });
    }
};
                

JOIN Table with Additional Attributes


// Event Registration System
module.exports = {
    up: async (queryInterface, Sequelize) => {
        await queryInterface.createTable('EventParticipants', {
            eventId: {
                type: Sequelize.INTEGER,
                references: {
                    model: 'Events',
                    key: 'id'
                },
                onDelete: 'CASCADE'
            },
            participantId: {
                type: Sequelize.INTEGER,
                references: {
                    model: 'Users',
                    key: 'id'
                },
                onDelete: 'CASCADE'
            },
            registrationDate: {
                type: Sequelize.DATE,
                defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
            },
            status: {
                type: Sequelize.ENUM('pending', 'confirmed', 'cancelled'),
                defaultValue: 'pending'
            },
            paymentStatus: {
                type: Sequelize.BOOLEAN,
                defaultValue: false
            },
            notes: {
                type: Sequelize.TEXT
            }
        });
    }
};
                

Composite Primary Keys in JOIN Tables


module.exports = {
    up: async (queryInterface, Sequelize) => {
        await queryInterface.createTable('TeamMembers', {
            teamId: {
                type: Sequelize.INTEGER,
                primaryKey: true,
                references: {
                    model: 'Teams',
                    key: 'id'
                },
                onDelete: 'CASCADE'
            },
            userId: {
                type: Sequelize.INTEGER,
                primaryKey: true,
                references: {
                    model: 'Users',
                    key: 'id'
                },
                onDelete: 'CASCADE'
            },
            role: {
                type: Sequelize.STRING,
                allowNull: false
            }
        });
    }
};
                

Best Practices

  • Meaningful Names: Use clear, descriptive names for JOIN tables
  • Consider Additional Fields: Add relevant metadata to JOIN tables
  • Use Appropriate Constraints: Implement proper foreign key constraints
  • Handle Deletions: Choose appropriate onDelete behaviors
  • Index Properly: Add indexes for frequently queried columns

Common Pitfalls to Avoid

  • Forgetting to add foreign key constraints
  • Missing indexes on frequently joined columns
  • Not handling deletion cases properly
  • Overlooking unique constraints when needed
  • Incorrect migration order

Advanced Topics to Explore

  • Self-referential many-to-many relationships
  • Polymorphic associations
  • Temporal tables for historical data
  • Optimization strategies
  • Complex migration patterns