简体   繁体   中英

How to use 3 foreign keys in many-to-many join table in Sequelize

I want this:

在此处输入图像描述

I can do this manually w/ SQL in a few minutes and it's a very common scenario, so I have to think there's a way in Sequelize.

Users can have many roles within many organizations. I could be an Admin for Acme but a mere User for Microsoft. Data that looks like so:

User data:

在此处输入图像描述

Org data:

在此处输入图像描述

Roles:

在此处输入图像描述

Then of course, I can pull it all together:

select
    u.username,
    r.name,
    o.name
from
    "user" u
inner join
    user_role_organization uro on u.id = uro.user_id
inner join
    organization o on uro.organization_id = o.id
inner join
    role r on uro.role_id = r.id

在此处输入图像描述

The REAL world model I'm working in looks like so:

const orgModel = {
    id: {
        type: DataTypes.UUID,
        primaryKey: true,
        allowNull: false
    },
    name: {
        type: DataTypes.STRING(100),
        allowNull: false
    }
};
const roleModel = {
    id: {
        type: DataTypes.UUID,
        primaryKey: true,
        allowNull: false
    },
    name: {
        type: DataTypes.STRING(100),
        allowNull: false
    }
};
const userModel = {
    id: {
        type: DataTypes.UUID,
        primaryKey: true,
        allowNull: false
    },
    username: {
        type: DataTypes.STRING(100),
        allowNull: false
    }
};
const organizationUserToRoleModel = {
    id : {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true
    },
    organization_id: {
        type: DataTypes.UUID,
        allowNull: false
    },
    role_id: {
        type: DataTypes.UUID,
        allowNull: false
    },
    user_id: {
        type: DataTypes.UUID,
        allowNull: false
    }
};

...and their respective relationships

auth_user.belongsToMany(auth_organization, { as: "AuthOrganizations", through: organization_to_user_to_role, foreignKey: "user_id" });
auth_organization.belongsToMany(auth_user, { as: "AuthUsers", through: organization_to_user_to_role, foreignKey: "organization_id" });

auth_organization.belongsToMany(role, { as: "Roles", through: organization_to_user_to_role, foreignKey: "organization_id" });
role.belongsToMany(auth_organization, { as: "RoleOrganizations", through: organization_to_user_to_role, foreignKey: "role_id" });

auth_user.belongsToMany(role, { as: "OrganizationUserRoles", through: organization_to_user_to_role, foreignKey: "user_id" });
role.belongsToMany(auth_user, { as: "OrganizationRoleUsers", through: organization_to_user_to_role, foreignKey: "role_id" });

I end up with something that looks correct:

在此处输入图像描述

However, I get the following error when seeding similar data:

ValidationErrorItem {
  message: 'organization_id must be unique',
  type: 'unique violation',
  path: 'organization_id',
  value: '385e2860-094d-11ed-a072-25e64f3c77e7',
  origin: 'DB',
  instance: null,
  validatorKey: 'not_unique',
  validatorName: null,
  validatorArgs: []
}

Makes no sense that anything but "id" would need to be unique in that table, no? I guess it's forcing uniqueness due to being a foreign key? I got to this using values populated like so:

let acmeOrg = await auth_organization.findOne({ where: { name: "ACME Corp." } });
let fakeOrg = await auth_organization.findOne({ where: { name: "Fake, Inc." } });

let user1 = await auth_user.findOne({ where: { username: "user1" } });
let user2 = await auth_user.findOne({ where: { username: "user2" } });

let ownerRole = await role.findOne({ where: { name: "Owner" } });
let adminRole = await role.findOne({ where: { name: "Admin" } });
let userRole = await role.findOne({ where: { name: "User" } });

await user1.addAuthOrganizations(acmeOrg, 
    { 
        through: { 
            role_id: ownerRole.id
        } 
    });
await user2.addAuthOrganizations(acmeOrg, 
    { 
        through: { 
            role_id: adminRole.id
        } 
    });
await user1.addAuthOrganizations(fakeOrg, 
    { 
        through: { 
            role_id: userRole.id
        } 
    });

I have more history w/ relational data than I do Sequelize. I had also tried this model for the join table, which created a much stranger model that was forcing a composite primary key on the user_id and organization_id fields, even if I set primaryKey: false.

EDIT 1 :

I suspect it's all in how I build the FKs for the model, just from prior Sequelize adventures. I just tried setting unique to false and setting the FKs like so - it now complains that "user_id" must be unique, even though that's not true, at least according to my intentions.

let organizationUserToRoleModel = {
    id: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true
    },
    organization_id: {
        type: DataTypes.UUID,
        allowNull: false,
        unique: false
    },
    role_id: {
        type: DataTypes.UUID,
        allowNull: false,
        unique: false
    },
    user_id: {
        type: DataTypes.UUID,
        allowNull: false,
        unique: false
    }
};

auth_user.belongsToMany(auth_organization, { as: "AuthUserOrganizations", through: organization_to_user_to_role, foreignKey: "user_id" });
auth_organization.belongsToMany(auth_user, { as: "OrganizationAuthUsers", through: organization_to_user_to_role, foreignKey: "organization_id" });

auth_organization.belongsToMany(role, { as: "AuthOrganizationRoles", through: organization_to_user_to_role, foreignKey: "organization_id" });
role.belongsToMany(auth_organization, { as: "RoleAuthOrganizations", through: organization_to_user_to_role, foreignKey: "role_id" });

EDIT 2:

Found the cause, No matter what I do to the model. unique constraints are added to the foreign keys: Here's the latest model for the join table:

let organizationUserToRoleModel = {
    id: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true
    },
    organization_id: {
        type: DataTypes.UUID,
        allowNull: true,
        constraints: false,
        unique: false
    },
    role_id: {
        type: DataTypes.UUID,
        allowNull: true,
        constraints: false,
        unique: false
    },
    user_id: {
        type: DataTypes.UUID,
        allowNull: true,
        constraints: false,
        unique: false
    }
};

However, they are still created when I inspect the result:

ALTER TABLE auth.organization_to_user_to_role ADD CONSTRAINT organization_to_user_to_role_organization_id_role_id_key UNIQUE (organization_id, role_id)

ALTER TABLE auth.organization_to_user_to_role ADD CONSTRAINT organization_to_user_to_role_user_id_key UNIQUE (user_id)

If I manually remove them, I can seed the expected data and query it w/oa problem, like this:

select
    u.username
from
    auth_user u
inner join
    organization_to_user_to_role our
    on u.id = our.user_id 
inner join
    auth_organization ao 
    on ao.id = our.organization_id 
inner join
    "role" r 
    on r.id = our.role_id 

I feel like I'm super close, but not sure how to prevent the FK constraints from being created. Setting constraints to false seemingly does nothing here. I suppose I could code their removal, after the fact, but that seems hacky and incorrect.

EDIT 3:

I've tried a few different things on the model itself, as well as how the keys relate, but I'm getting the exact same result with the exact same unique constraints. If I could even make it set a single unique constraint on all 3 keys (now that they're all part of the compound key), that would suffice.

Current model, which I prefer:

let organizationUserToRoleModel = {
    organization_id: {
        type: DataTypes.UUID,
        primaryKey: true,
        constraints: false,
        unique: false
    },
    role_id: {
        type: DataTypes.UUID,
        primaryKey: true,
        constraints: false,
        unique: false
    },
    user_id: {
        type: DataTypes.UUID,
        primaryKey: true,
        constraints: false,
        unique: false
    }
};

It seems as if "constraints" and "unique" are having zero effect. The only difference this has compared to my earlier attempts, is that the compound key makes more sense than a useless auto-increment PK.

Let me see if I can simplify the question ... You have three Postgres tables: Organization, Role and User. Each have unique identifiers.

A User is allowed only one role in a given Organization, but could be associated to multiple Organizations with a different role in each (your example an Admin for Acme, but a User for Microsoft).

What does the model need to look like in Sequelize to make that happen?

Is that about right?

Looks like you got there.

In general, any table providing a set of FK values in a model requires that the value column itself to be declared unique (at least). In some models, the table providing the key values have the Primary Key set to the actual data values, rather than to an ID column.

Your design has problems see the James Barton answer here . I suggest you have the following tables instead of User_Role_Organization:

  1. Role_User

  2. Organization_Role

If a role belongs to multiple organization then all the role users are also belong to those organizations which probably is not intended.

The answer was to just force the table to work how I wanted. It works fine in Sequelize.

The migration made the change and reading/writing works flawlessly in Sequelize. So, just a bug I suppose. Most ORMs seem to have funny edge cases like this.

    await queryInterface.createTable("organization_to_user_to_role", {
        organization_id: {
            type: Sequelize.DataTypes.UUID,
            primaryKey: true,
            constraints: false
        },
        role_id: {
            type: Sequelize.DataTypes.UUID,
            primaryKey: true,
            constraints: false
        },
        user_id: {
            type: Sequelize.DataTypes.UUID,
            primaryKey: true,
            constraints: false
        }
    });

    queryInterface.addConstraint("organization_to_user_to_role", {
        type: "FOREIGN KEY",
        name: "organization_to_user_to_role_organization_id_fkey",
        fields: ["organization_id"],
        references: {
            table: "auth_organization",
            field: "id"
        }
    });
    queryInterface.addConstraint("organization_to_user_to_role", {
        type: "FOREIGN KEY",
        name: "organization_to_user_to_role_user_id_fkey",
        fields: ["user_id"],
        references: {
            table: "auth_user",
            field: "id"
        }
    });
    queryInterface.addConstraint("organization_to_user_to_role", {
        type: "FOREIGN KEY",
        name: "organization_to_user_to_role_role_id_fkey",
        fields: ["role_id"],
        references: {
            table: "role",
            field: "id"
        }
    });

If you generate a new database, you'll have to remove the unwanted constraints, which I put into my migration with a condition, in case they exist:

        queryInterface.removeConstraint(
            "organization_to_user_to_role"
            "organization_to_user_to_role_organization_id_key"
        );
        queryInterface.removeConstraint(
            "organization_to_user_to_role",
            "organization_to_user_to_role_role_id_user_id_key"
        );

ok, i understand this problem. I will give the same example: in test.js

const module = require('./module')(sequelize, DataTypes)
    const permission = require('./permission')(sequelize, DataTypes)
    const role = require('./role')(sequelize, DataTypes)
    const test = sequelize.define('test', {
        moduleId: {
            type: DataTypes.INTEGER(4),
            ref: {
                model: module,
                key: 'id'
            }
        },
        roleId: {
            type: DataTypes.INTEGER(4),
            ref: {
                model: role,
                key: 'id'
            }
        },
        permissionId: {
            type: DataTypes.INTEGER(4),
            ref: {
                model: permission,
                key: 'id'
            }
        },
        createdDate: {
            type: DataTypes.DATE,
            defaultValue: DataTypes.NOW
        }
    }, {
        timestamps: false
    });

    test.belongsTo(module, { foreignKey: 'moduleId' });
    test.belongsTo(permission, { foreignKey: 'permissionId' });
    test.belongsTo(role, { foreignKey: 'roleId' });

    module.hasMany(test, { foreignKey: 'moduleId' });
    permission.hasMany(test, { foreignKey: 'permissionId' });
    role.hasMany(test, { foreignKey: 'roleId' });

    

    return test;

now, we can add only one unique constraint

const queryInterface = sequelize.getQueryInterface();


sequelize.sync({force: true}).then(()=>{
    return queryInterface.addConstraint('tests',  {
        fields:['moduleId', 'permissionId', 'roleId'],
        type: 'unique',
        name: 'custom_unique_constraint'
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM