如何在sequelize中对联接表上的额外列进行ORM?

13

我正在使用node v9.5和sequelize v4.33(postgres方言)。

我有两个一级模型:Driver(特定的人)和Car(通用的车型组合)。到目前为止,它们通过多对多的连接表进行连接。现在我想开始跟踪该连接表上的其他属性,但我在声明这些关系时遇到了问题,无法使它们正常工作。

const Driver = sqlz.define('Driver', {
    id: { primaryKey: true, type: DataTypes.UUID },
    name: DataTypes.string
})

const Car = sqlz.define('Car', {
    id: { primaryKey: true, type: DataTypes.UUID },
    make: DataTypes.string,
    model: DataTypes.string
})

// old associations; worked great when requirements were simpler
Driver.belongsToMany(Car, {
    through: 'DriverCar',
    as: 'carList',
    foreignKey: 'driverId'
})

Car.belongsToMany(Driver, {
    through: 'DriverCar',
    as: 'driverList',
    foreignKey: 'carId'
})

现在我想要开始跟踪有关汽车及其驾驶员之间更多信息,例如特定汽车的颜色。

步骤1:我更新迁移脚本,向联接表添加一个新列,如下所示:

queryInterface.createTable( 'DriverCar', {
    driverId: {
        type: sqlz.UUID,
        allowNull: false,
        primaryKey: true,
        references: {
            model: 'Driver',
            key: 'id'
        }
    },
    carId: {
        type: sqlz.UUID,
        allowNull: false,
        primaryKey: true,
        references: {
            model: 'Car',
            key: 'id'
        }
    },
    createdAt: {
        type: sqlz.DATE,
        allowNull: false
    },
    updatedAt: {
        type: sqlz.DATE,
        allowNull: false
    },

    // new column for join table
    color: {
      type: Sequelize.STRING
    }
})

步骤2:我为DriverCar定义了一个新的SQLZ模型:

const DriverCar = sqlz.define('DriverCar', {
    color: DataTypes.string
})

我假设只需要定义有趣的属性,并且driverIdcarId将仍然从将要被定义的关联中推断出。

步骤3:我需要更新DriverCarDriverCar之间存在的关联。

这就是我卡住的地方。我尝试更新现有的关联,如下所示:

Driver.belongsToMany(Car, {
    through: DriverCar, // NOTE: no longer a string, but a reference to new DriverCar model
    as: 'carList',
    foreignKey: 'driverId'
})

Car.belongsToMany(Driver, {
    through: DriverCar, // NOTE: no longer a string, but a reference to new DriverCar model
    as: 'driverList',
    foreignKey: 'carId'
})

这段代码没有报错,但是当我尝试调用driver.getCarList()时,并没有从连接表中获取到新的color属性。(Sqlz被配置为记录每个SQL语句,我已经验证没有从连接表中请求任何属性。)

因此,我尝试更明确地定义这种关系,将DriverDriverCar相关联,然后将CarDriverCar相关联:

// Driver -> Car
Driver.hasMany(DriverCar, {
    as: 'carList',
    foreignKey: 'driverId'
})

// Car -> Driver
Car.hasMany(DriverCar, {
    foreignKey: 'carId'
})

我还告诉sqlz DriverCar 不会有一个标准的行 id:

DriverCar.removeAttribute('id')

目前为止,请求 Driver 的车辆列表 (driver.getCarList()) 似乎可以工作,因为我可以在 SQL 中看到联接表属性被提取。但是保存操作失败:

driverModel.setCarList([ carModel1 ])

UPDATE DriverCar
SET "driverId"='a-uuid',"updatedAt"='2018-02-23 22:01:02.126 +00:00'
WHERE "undefined" in (NULL)

错误:

SequelizeDatabaseError: column "undefined" does not exist

我认为这个错误的原因是sqzl不理解如何正确地识别连接表中的行,因为我没有建立必要的关联。而且说实话,我不确定我是否做得正确;我对ORM还很陌生,但我期望我需要指定4个关联:

  1. Driver -> DriverCar
  2. DriverCar -> Car
  3. Car -> DriverCar
  4. DriverCar -> Driver

简而言之,我有两个一级实体,它们在多对多的关系中连接。我试图向关系中添加数据,发现ORM需要以不同方式定义这些关联,并且在表达新关联时遇到麻烦。


这是 sequelize 并不擅长的事情之一。但你仍应该能够找到 Driver 并包含 Cars,它应该会带来所有字段。在查询中使用 include 来完成,而不是使用 magic method。 - yBrodsky
@yBrodsky:谢谢你的提示。我相信我知道你所说的“不使用魔法方法”的意思,但我需要做更多的研究来理解你的建议。 - Tom
我应该补充说明,sequelize 上的这个问题表明 include 方法行不通: https://github.com/sequelize/sequelize/issues/9094 - Tom
还是没有运气。如果有人看到这个,我仍然会感激帮助。 - Tom
这个程序不起作用吗? - yBrodsky
1个回答

27

有关您的别名的注释

在回答之前,我想指出您选择的别名(carListdriverList)可能不是最好的选择,因为虽然自动生成的sequelize方法.setCarList().setDriverList()很合理,但.addCarList().addDriverList().removeCarList().removeDriverList()这些方法是无意义的,因为它们只接受单个实例作为参数,而不是列表。

对于我的答案,我不会使用任何别名,并让Sequelize默认使用.setCars().setDrivers().addCar().removeCar()等方法,这对我来说更有意义。


工作代码示例

我已经制作了一个100%自包含的代码来测试此功能。只需复制粘贴并运行它(在运行npm install sequelize sqlite3之后):

const Sequelize = require("sequelize");
const sequelize = new Sequelize({ dialect: 'sqlite', storage: 'db.sqlite' });

const Driver = sequelize.define("Driver", {
    name: Sequelize.STRING
});
const Car = sequelize.define("Car", {
    make: Sequelize.STRING,
    model: Sequelize.STRING
});
const DriverCar = sequelize.define("DriverCar", {
    color: Sequelize.STRING
});
Driver.belongsToMany(Car, { through: DriverCar, foreignKey: "driverId" });
Car.belongsToMany(Driver, { through: DriverCar, foreignKey: "carId" });

var car, driver;

sequelize.sync({ force: true })
    .then(() => {
        // Create a driver
        return Driver.create({ name: "name test" });
    })
    .then(created => {
        // Store the driver created above in the 'driver' variable
        driver = created;

        // Create a car
        return Car.create({ make: "make test", model: "model test" });
    })
    .then(created => {
        // Store the car created above in the 'car' variable
        car = created;

        // Now we want to define that car is related to driver.
        // Option 1:
        return car.addDriver(driver, { through: { color: "black" }});

        // Option 2:
        // return driver.setCars([car], { through: { color: "black" }});

        // Option 3:
        // return DriverCar.create({
        //     driverId: driver.id,
        //     carId: car.id,
        //     color: "black"
        // });
    })
    .then(() => {
        // Now we get the things back from the DB.

        // This works:
        return Driver.findAll({ include: [Car] });

        // This also works:
        // return car.getDrivers();

        // This also works:
        // return driver.getCars();
    })
    .then(result => {
        // Log the query result in a readable way
        console.log(JSON.stringify(result.map(x => x.toJSON()), null, 4));
    });

上述代码的日志输出如预期(至少我是这样认为的):

[
    {
        "id": 1,
        "name": "name test",
        "createdAt": "2018-03-11T03:04:28.657Z",
        "updatedAt": "2018-03-11T03:04:28.657Z",
        "Cars": [
            {
                "id": 1,
                "make": "make test",
                "model": "model test",
                "createdAt": "2018-03-11T03:04:28.802Z",
                "updatedAt": "2018-03-11T03:04:28.802Z",
                "DriverCar": {
                    "color": "black",
                    "createdAt": "2018-03-11T03:04:28.961Z",
                    "updatedAt": "2018-03-11T03:04:28.961Z",
                    "driverId": 1,
                    "carId": 1
                }
            }
        ]
    }
]

请注意,没有秘密。请观察您正在查找的额外属性color是嵌套在查询结果中而不是CarDriver的同一层次结构中。这是Sequelize的正确行为。

确保您可以运行此代码并获得与我相同的结果。我的Node版本不同,但我怀疑这可能与任何相关事项无关。然后,将我的代码与您的代码进行比较,看看是否能够找出导致问题的原因。如果您需要进一步的帮助,请随时在评论中提问 :)


关于具有额外字段的多对多关系的说明

由于我自己遇到了这个问题,并且这与您的情况有关,所以我认为我应该在我的答案中添加一个部分,提醒您设置过于复杂的多对多关系的“陷阱”(这是我自己苦苦挣扎后学到的课程)。

我不想重复自己,所以我只会简要引用我在Sequelize问题9158中所说的内容,并添加进一步阅读链接:

连接表是存在于关系数据库中来表示多对多关系的表,最初只有两个字段(定义多对多关系的每个表的外键)。虽然确实可以在该表上定义额外的字段/属性,即关联本身的额外属性(如您在问题标题中提到的),但应该注意:如果它变得过于复杂,则应将其“升级”为一个完整的实体。

进一步阅读:


1
@Tom - 很抱歉回复这么晚。关于你提到的复数问题,请参见我的问题和答案这里这里。但我建议你分步解决你的问题。首先,不要考虑别名,看看能否让你的代码在没有别名的情况下工作。然后,在完成此操作后,再次添加你的别名,看看会发生什么。如果你需要进一步帮助,请让我知道。如果你成功了,请告诉我,并且如果我有帮助,请点击绿色复选标记 :) - Pedro A
1
@bluehipy 你好,我明天会仔细看一下 :) - Pedro A
1
不要在生产数据库中使用带有数据的 sequelize.sync({ force: true }),而应该使用迁移文件。 - Kenzo
兄弟,有一个多态关系的生产环境。感谢你的帮助,我完成了缺失的部分。干杯! - HyopeR
@HyopeR 很棒!!干杯 :) - Pedro A
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接