【问题标题】:FindAll with includes involving a complicated many-to-(many-to-many) relationship (sequelizejs)FindAll 包含涉及复杂的多对(多对多)关系(sequelizejs)
【发布时间】:2018-07-20 15:33:25
【问题描述】:

这里有a sibling question in Software Engineering SE

考虑CompanyProductPerson

CompanyProduct 之间存在多对多关系,通过联结表 Company_Product,因为给定的公司可能生产不止一种产品(例如“汽车”和“自行车”) ,而且一个给定的产品,比如“汽车”,可以由多家公司生产。在联结表Company_Product 中有一个额外的字段“价格”,它是给定公司销售给定产品的价格。

Company_ProductPerson 之间还有另一个多对多关系,通过联结表 Company_Product_Person。是的,它是一种多对多关系,涉及一个已经是联结表的实体。这是因为一个人可以拥有多个产品,例如 company1 的汽车和 company2 的自行车,而同一个 company_product 可以由多个人拥有,因为例如 person1 和 person2 都可能从公司 1.在联结表Company_Product_Person 中有一个额外的字段“想法”,其中包含该人在购买 company_product 时的想法。

我想使用sequelize 进行查询,以从数据库中获取Company 的所有实例,以及所有相关的Products 和各自的Company_Product,这反过来又包括所有相关的Persons 和各自的@ 987654338@.

获取两个联结表的元素也很重要,因为“价格”和“想法”字段很重要。

我不知道该怎么做。

为了调查这个问题,我尽量缩短了代码。 看起来很大,但大部分都是模型声明样板:(要运行它,首先要执行npm install sequelize sqlite3

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

// ================= MODELS =================

const Company = sequelize.define("company", {
    id: {
        type: Sequelize.INTEGER,
        allowNull: false,
        autoIncrement: true,
        primaryKey: true
    },
    name: Sequelize.STRING
});

const Product = sequelize.define("product", {
    id: {
        type: Sequelize.INTEGER,
        allowNull: false,
        autoIncrement: true,
        primaryKey: true
    },
    name: Sequelize.STRING
});

const Person = sequelize.define("person", {
    id: {
        type: Sequelize.INTEGER,
        allowNull: false,
        autoIncrement: true,
        primaryKey: true
    },
    name: Sequelize.STRING
});

const Company_Product = sequelize.define("company_product", {
    id: {
        type: Sequelize.INTEGER,
        allowNull: false,
        autoIncrement: true,
        primaryKey: true
    },
    companyId: {
        type: Sequelize.INTEGER,
        allowNull: false,
        references: {
            model: "company",
            key: "id"
        },
        onDelete: "CASCADE"
    },
    productId: {
        type: Sequelize.INTEGER,
        allowNull: false,
        references: {
            model: "product",
            key: "id"
        },
        onDelete: "CASCADE"
    },
    price: Sequelize.INTEGER
});

const Company_Product_Person = sequelize.define("company_product_person", {
    id: {
        type: Sequelize.INTEGER,
        allowNull: false,
        autoIncrement: true,
        primaryKey: true
    },
    companyProductId: {
        type: Sequelize.INTEGER,
        allowNull: false,
        references: {
            model: "company_product",
            key: "id"
        },
        onDelete: "CASCADE"
    },
    personId: {
        type: Sequelize.INTEGER,
        allowNull: false,
        references: {
            model: "person",
            key: "id"
        },
        onDelete: "CASCADE"
    },
    thoughts: Sequelize.STRING
});

// ================= RELATIONS =================

// Many to Many relationship between Company and Product
Company.belongsToMany(Product, { through: "company_product", foreignKey: "companyId", onDelete: "CASCADE" });
Product.belongsToMany(Company, { through: "company_product", foreignKey: "productId", onDelete: "CASCADE" });

// Many to Many relationship between Company_Product and Person
Company_Product.belongsToMany(Person, { through: "company_product_person", foreignKey: "companyProductId", onDelete: "CASCADE" });
Person.belongsToMany(Company_Product, { through: "company_product_person", foreignKey: "personId", onDelete: "CASCADE" });

// ================= TEST =================

var company, product, person, company_product, company_product_person;

sequelize.sync({ force: true })
    .then(() => {
        // Create one company, one product and one person for tests.
        return Promise.all([
            Company.create({ name: "Company test" }).then(created => { company = created }),
            Product.create({ name: "Product test" }).then(created => { product = created }),
            Person.create({ name: "Person test" }).then(created => { person = created }),
        ]);
    })
    .then(() => {
        // company produces product
        return company.addProduct(product);
    })
    .then(() => {
        // Get the company_product for tests
        return Company_Product.findAll().then(found => { company_product = found[0] });
    })
    .then(() => {
        // person owns company_product
        return company_product.addPerson(person);
    })
    .then(() => {
        // I can get the list of Companys with their Products, but couldn't get the nested Persons...
        return Company.findAll({
            include: [{
                model: Product
            }]
        }).then(companies => {
            console.log(JSON.stringify(companies.map(company => company.toJSON()), null, 4));
        });
    })
    .then(() => {
        // And I can get the list of Company_Products with their Persons...
        return Company_Product.findAll({
            include: [{
                model: Person
            }]
        }).then(companyproducts => {
            console.log(JSON.stringify(companyproducts.map(companyproduct => companyproduct.toJSON()), null, 4));
        });
    })
    .then(() => {
        // I should be able to make both calls above in one, getting those nested things
        // at once, but how??
        return Company.findAll({
            include: [{
                model: Product
                // ???
            }]
        }).then(companies => {
            console.log(JSON.stringify(companies.map(company => company.toJSON()), null, 4));
        });
    });

我的目标是一次性获得一组Companys 以及所有深层嵌套的PersonsCompany_Product_Persons

// My goal:
[
    {
        "id": 1,
        "name": "Company test",
        "createdAt": "...",
        "updatedAt": "...",
        "products": [
            {
                "id": 1,
                "name": "Product test",
                "createdAt": "...",
                "updatedAt": "...",
                "company_product": {
                    "id": 1,
                    "companyId": 1,
                    "productId": 1,
                    "price": null,
                    "createdAt": "...",
                    "updatedAt": "...",
                    "persons": [
                        {
                            "id": 1,
                            "name": "Person test",
                            "createdAt": "...",
                            "updatedAt": "...",
                            "company_product_person": {
                                "id": 1,
                                "companyProductId": 1,
                                "personId": 1,
                                "thoughts": null,
                                "createdAt": "...",
                                "updatedAt": "..."
                            }
                        }
                    ]
                }
            }
        ]
    }
];

我该怎么做?

注意:我可以分别进行这两个查询并编写一些代码来“加入”检索到的对象,但这在计算上会很昂贵且很难看。我正在寻找正确的方法来做到这一点。

【问题讨论】:

    标签: javascript node.js many-to-many sequelize.js eager-loading


    【解决方案1】:

    在这里。


    简答

    解决方案的关键是重新考虑关联。将关联更改为:

    Company.hasMany(Company_Product, { foreignKey: "companyId" });
    Company_Product.belongsTo(Company, { foreignKey: "companyId" });
    
    Product.hasMany(Company_Product, { foreignKey: "productId" });
    Company_Product.belongsTo(Product, { foreignKey: "productId" });
    
    Company_Product.hasMany(Company_Product_Person, { foreignKey: "companyProductId" });
    Company_Product_Person.belongsTo(Company_Product, { foreignKey: "companyProductId" });
    
    Person.hasMany(Company_Product_Person, { foreignKey: "personId" });
    Company_Product_Person.belongsTo(Person, { foreignKey: "personId" });
    

    return company.addProduct(product);更改为

    return Company_Product.create({
        companyId: company.id,
        productId: product.id,
        price: 99
    }).then(created => { company_product = created });
    

    return company_product.addPerson(person) 更改为

    return Company_Product_Person.create({
        companyProductId: company_product.id,
        personId: person.id,
        thoughts: "nice"
    }).then(created => { company_product_person = created });
    

    回答问题的查询是

    Company.findAll({
        include: [{
            model: Company_Product,
            include: [{
                model: Product
            }, {
                model: Company_Product_Person,
                include: [{
                    model: Person
                }]
            }]
        }]
    })
    

    生成的 JSON 结构并不完全是所提到的“目标”,而只是重新排序的问题。


    长答案

    我找到了一个解决方案,该解决方案涉及重新处理表之间的关联,即使问题中给出的关联在技术上并没有错。一种看待问题的新方法,即改变关联,是找到一种方法来做我想做的事情的关键。

    分析旧方法

    首先,我的问题中给出的两个联结表都不仅仅是“只是”联结表。它们不仅仅是一个定义哪些元素与哪些元素相关的工具,它们还有更多意义:

    • 他们还有额外的信息(分别是“价格”和“想法”字段);

    • 第一个 Company_Product 本身也与其他表有关系。

    严格来说,这在技术上并没有错,但是有一种更自然的方式来构建数据库以表示相同的事物。更好的是,使用这种新方法,我想要的查询变得非常简单。

    解决方案:新方法

    当我们看到我们正在对可以购买的物品和购买本身进行建模时,解决方案就会出现。与其将这些信息“伪装”在多对多关系的联结表中,不如在我们的方案中将它们作为显式实体,并使用它们自己的表。

    所以,首先,澄清一下,让我们重命名我们的模型:

    • CompanyCompany
    • Product 变为 ProductType
    • Company_Product 变为 Product
    • PersonPerson
    • Company_Product_Person 变为 Purchase

    然后我们看到:

    • 一个Product 有一个Company 和一个ProductType。反之,同一个Company可以关联多个Product,同一个ProductType可以关联多个Product
    • 一个Purchase 有一个Product 和一个Person。反之,同一个Product可以关联多个Purchase,同一个Product可以关联多个Person

    请注意,不再存在多对多关系。关系变为:

    Company.hasMany(Product, { foreignKey: "companyId" });
    Product.belongsTo(Company, { foreignKey: "companyId" });
    
    ProductType.hasMany(Product, { foreignKey: "productTypeId" });
    Product.belongsTo(ProductType, { foreignKey: "productTypeId" });
    
    Product.hasMany(Purchase, { foreignKey: "productId" });
    Purchase.belongsTo(Product, { foreignKey: "productId" });
    
    Person.hasMany(Purchase, { foreignKey: "personId" });
    Purchase.belongsTo(Person, { foreignKey: "personId" });
    

    然后,旧的company.addProduct(product);变成了

    Product.create({
        companyId: company.id
        productTypeId: productType.id,
        price: 99
    })
    

    类似地,company_product.addPerson(person); 变为

    Purchase.create({
        productId: product.id,
        personId: person.id,
        thoughts: "nice"
    })
    

    现在,我们可以很容易地看到进行所需查询的方法:

    Company.findAll({
        include: [{
            model: Product,
            include: [{
                model: ProductType
            }, {
                model: Purchase,
                include: [{
                    model: Person
                }]
            }]
        }]
    })
    

    上述查询的结果并不是100%等价于问题中提到的“目标”,因为Product和ProductType的嵌套顺序是交换的(Person和Purchase也是如此),但是转换成想要的结构是现在只需编写一些 javascript 逻辑,不再是涉及数据库或 sequelize 的问题。

    结论

    虽然问题中提供的数据库方案在技术上并没有错本身,但通过稍微改变方案找到了解决方案。

    我们不再使用不仅仅是简单的联结表的联结表,而是摆脱了多对多关系并将联结表“提升”为我们方案的成熟实体。事实上,表格是一样的;变化只是关系和看待它们的方式。

    【讨论】:

    • 不应该是“非常非常长的答案”吗?
    • @guilhermecgs 也许我应该添加更多细节:P
    猜你喜欢
    • 2022-01-06
    • 2010-12-11
    • 1970-01-01
    • 2016-11-03
    • 2015-09-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多