How do I ORM additional columns on a join table in sequelize?

前端 未结 1 388
挽巷
挽巷 2021-02-04 13:53

I\'m using node v9.5, sequelize v4.33 (postgres dialect).

I have two first-class models: Driver (specific people) and Car (generic make+model c

1条回答
  •  隐瞒了意图╮
    2021-02-04 14:35

    A note about your aliases

    Before going to the answer, I would like to point out that your choice of aliases (carList and driverList) could be better, because although the auto-generated sequelize methods .setCarList() and .setDriverList() do make sense, the methods .addCarList(), .addDriverList(), .removeCarList() and .removeDriverList() are nonsense, since they take only a single instance as a parameter, not a list.

    For my answer, I won't use any aliases, and let Sequelize default to .setCars(), .setDrivers(), .addCar(), .removeCar(), etc, which make much more sense to me.


    Example of working code

    I've made a 100% self-contained code to test this. Just copy-paste it and run it (after running 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));
        });
    

    The code above logs as expected (as I would expect, at least):

    [
        {
            "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
                    }
                }
            ]
        }
    ]
    

    Note that there is no secret. Observe that the extra attribute that you're looking for, color, comes nested in the query result, not in the same nesting level of the Car or Driver. This is the correct behavior of Sequelize.

    Make sure you can run this code and get the same result I do. My version of Node is different but I doubt that could be related to anything. Then, compare my code to your code and see if you can figure out what is causing you problems. If you need further help, feel free to ask in a comment :)


    A note about many-to-many relationships with extra fields

    Since I stumbled myself upon problems with this, and this is related to your situation, I thought I should add a section in my answer alerting you to the "trap" of setting up an overcomplicated many-to-many relationship (it's a lesson that I learned myself after struggling for a while).

    Instead of repeating myself, I will just add a brief quote of what I said in Sequelize Issue 9158, and add links for further reading:

    Junction tables, the tables that exist in relational databases to represent many-to-many relationships, initially have only two fields (the foreign keys of each table defining the many-to-many relationship). While it's true that it's possible to define extra fields/properties on that table, i.e. extra properties for the association itself (as you put in the issue title), care should be taken here: if it's getting overcomplicated, it's a sign that you should "promote" your junction table to a full-fledged entity.

    Further reading:

    • My own self-answered question involving an overcomplicated setup of many-to-many relationships in sequelize: FindAll with includes involving a complicated many-to-(many-to-many) relationship (sequelizejs)
    • And its sibling question: Is it OK to have a many-to-many relationship where one of the tables involved is already a junction table?

    0 讨论(0)
提交回复
热议问题