FindAll with incluye una relación complicada de muchos a muchos (sequelizejs)

Esto tiene una pregunta de hermanos en Software Engineering SE .

Considerar Company , Product y Person .

Existe una relación de muchos a muchos entre la Company y el Product , a través de la tabla de unión Company_Product , porque una compañía determinada puede producir más de un producto (como “automóvil” y “bicicleta”), pero también un producto determinado, como “coche”, puede ser producido por múltiples empresas. En la tabla de unión Company_Product hay un campo “precio” adicional, que es el precio en el que la compañía en cuestión vende el producto en Company_Product .

Existe otra relación de muchos a muchos entre Company_Product y Person , a través de una tabla de unión Company_Product_Person . Sí, es una relación de muchos a muchos que involucra a una entidad que ya es una tabla de unión. Esto se debe a que una persona puede poseer varios productos, como un automóvil de la compañía1 y una bicicleta de la compañía2, y, a su vez, el mismo producto de la empresa puede ser propiedad de más de una persona, ya que, por ejemplo, tanto la persona1 como la persona2 podrían haber comprado un automóvil empresa1. En la mesa de unión Company_Product_Person hay un campo adicional “pensamientos” que contiene los pensamientos de la persona en el momento en que compraron el producto de la compañía.

Quiero hacer una consulta con secuela para obtener de la base de datos todas las instancias de la Company , con todos los Products relacionados con la Company_Product respectiva, que a su vez incluye todas las Persons relacionadas con la Company_Product_Persons respectiva.

Obtener los elementos de ambas tablas de unión también es importante, porque los campos “precio” y “pensamientos” son importantes.

Y no pude averiguar cómo hacer esto.

Hice el código tan corto como pude para investigar esto. Parece grande, pero la mayor parte es un modelo de statement modelo: (para ejecutarlo, primero haga 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)); }); }); 

Mi objective es obtener una serie de Companys ya con todas las Persons profundamente anidadas y Company_Product_Persons a la vez:

 // 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": "..." } } ] } } ] } ]; 

¿Cómo puedo hacer esto?

Nota: podría hacer ambas consultas por separado y escribir algún código para “unir” los objetos recuperados, pero eso sería computacionalmente costoso y feo. Estoy buscando la manera correcta de hacer esto.

OP aquí.


Respuesta corta

La clave de la solución es repensar las asociaciones. Cambiar las asociaciones a:

 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" }); 

Cambiar return company.addProduct(product); a

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

Cambiar return company_product.addPerson(person) a

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

La consulta que responde a la pregunta es.

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

La estructura JSON resultante no es exactamente el “objective” mencionado en la pregunta, pero es solo una cuestión de reordenar.


Respuesta larga

Encontré una solución que implica volver a trabajar las asociaciones entre las tablas, aunque las asociaciones que figuran en la pregunta no son técnicamente incorrectas. Una nueva forma de ver el problema, cambiar las asociaciones, fue la clave para encontrar la manera de hacer lo que quería.

Analizando el antiguo enfoque.

En primer lugar, las dos tablas de conexiones que aparecen en mi pregunta eran más que “solo” tablas de conexiones. No eran simplemente una herramienta para definir qué elementos estaban relacionados con qué elementos, sino que eran algo más:

  • También tenían información adicional (los campos “precio” y “pensamientos”, respectivamente);

  • El primero, Company_Product , también tenía relaciones con otras tablas.

Esto no es técnicamente incorrecto, estrictamente hablando, pero hay una forma más natural de estructurar la base de datos para representar las mismas cosas. Y mejor, con este nuevo enfoque, hacer la consulta que quiero se vuelve muy simple.

Solución: nuevo enfoque

La solución aumenta cuando vemos que estamos modelando artículos que se pueden comprar y las compras en sí. En lugar de mantener esta información “disfrazada” dentro de la tabla de unión de una relación de muchos a muchos, los tendremos como entidades explícitas en nuestro esquema, con sus propias tablas.

Entonces, primero, para aclarar, cambiemos el nombre de nuestros modelos:

  • Company mantiene Company
  • Product convierte en ProductType
  • Company_Product convierte en Product
  • Person permanece Person
  • Company_Product_Person convierte en Purchase

Y luego vemos que:

  • Un Product tiene una Company y un ProductType . A la inversa, la misma Company puede relacionarse con múltiples Product y el mismo ProductType puede relacionarse con múltiples Product .
  • Una Purchase tiene un Product y una Person . A la inversa, el mismo Product puede estar relacionado con la Purchase múltiple y el mismo Product puede estar relacionado con la Person múltiple.

Tenga en cuenta que ya no hay relaciones de muchos a muchos. Las relaciones se convierten en:

 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" }); 

Y luego, la antigua company.addProduct(product); se convierte en

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

Y análogamente company_product.addPerson(person); se convierte en

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

Y ahora, podemos ver fácilmente la forma de realizar la consulta deseada:

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

El resultado de la consulta anterior no es 100% equivalente al “objective” mencionado en la pregunta, porque el orden de anidamiento de Product y ProductType se intercambia (y también lo es Person y Purchase), pero la conversión a la estructura deseada es ahora simplemente una cuestión de escribir alguna lógica javascript, y ya no es un problema que involucre bases de datos o secuelas.

Conclusión

Aunque el esquema de base de datos proporcionado en la pregunta no es técnicamente incorrecto per se , la solución se encontró cambiando el esquema un poco.

En lugar de usar tablas de unión que eran más que simples tablas de unión, eliminamos las relaciones de muchos a muchos y “promovimos” las tablas de unión a entidades de pleno derecho de nuestro esquema. De hecho, las tablas son las mismas; los cambios fueron solo en las relaciones y en la forma de mirarlos.

Intereting Posts