This is a Moleculer demo project which demonstrates that how you can create database relations between moleculer-db services following the one-database-per-service microservices concept.
This demo uses MongoDB and PostgreSQL in a mix in order to better demonstrate the power of populating between tables in different database engines.
flowchart TB
subgraph Mongo adapter
CUSTOMERS --- MongoDB((MongoDB))
PRODUCTS --- MongoDB
TAGS --- MongoDB
MongoDB --- MONGO_DB1[(DB: mol-demo-customers)]
MongoDB --- MONGO_DB2[(DB: mol-demo-products)]
MongoDB --- MONGO_DB3[(DB: mol-demo-tags)]
end
subgraph Sequelize adapter
ORDERS --- PG((PostgreSQL))
ORDER_ITEMS --- PG
PRODUCT_TAGS --- PG
PG --- PG_DB[(DB: mol-demo)]
end
This diagram shows the relations between the services.
erDiagram
CUSTOMERS {
string id PK
string name
string email
boolean active
}
ORDERS {
int id PK
datetime date
string customerId FK
decimal totalPrice
string status
}
ORDER_ITEMS {
int id PK
int orderId FK
string productId FK
int quantity
}
PRODUCTS {
string id PK
string name
decimal price
}
TAGS {
string id PK
string name
}
PRODUCT_TAGS {
int id PK
string productId FK
string tagId FK
}
CUSTOMERS ||--o{ ORDERS : ""
ORDERS ||--|{ ORDER_ITEMS : ""
PRODUCTS ||--o{ ORDER_ITEMS : ""
PRODUCTS ||--o{ PRODUCT_TAGS : ""
TAGS ||--o{ PRODUCT_TAGS : ""
The orders
service customerId
field is a reference to a customers
entity, so it represents a one-to-many relation.
In order to get the customer entity when listing orders, you should define a customer
populate in orders
service settings:
module.exports = {
name: "orders",
settings: {
fields: [
// ...
"customer",
// ...
],
// Populates for relations
populates: {
// ...
// The `customerId` field references to a customer.
customer: {
field: "customerId",
action: "customers.get",
params: {
fields: ["name", "email"]
}
},
// ...
}
}
};
You can find similar populates to cover references of
orderItems -> products
,product-tags -> products
orproduct-tags -> tags
.
await this.broker.call("orders.find", { populate: ["customer"] })
[
{
id: 89,
date: 2023-10-01T11:57:35.945Z,
customerId: '65195ead9693271664edc258',
customer: { name: 'John Doe', email: '[email protected]' },
status: 'completed'
},
{
id: 90,
date: 2023-10-01T11:57:36.063Z,
customerId: '65195ead9693271664edc259',
customer: { name: 'Jane Doe', email: '[email protected]' },
status: 'reserved'
}
]
To demonstrate the opposite direction, we can define another populate to get the orderItems
of the order
. So we define an items
populate which fetches all orderItems
entities based on the orderId
value.
module.exports = {
name: "orders",
settings: {
fields: [
// ...
"items",
// ...
],
// Populates for relations
populates: {
// ...
// The `customerId` field references to a customer.
/* Get the items from the `orderItems` service */
async items(ids, orders, rule, ctx) {
await Promise.all(orders.map(async order => {
order.items = await ctx.call("orderItems.find", {
query: {
orderId: order.id
}
});
}));
},
// ...
}
}
};
You can find similar populates to cover references of
product -> tags
orcustomers -> orders
.
await this.broker.call("orders.find", { populate: ["items"] })
[
{
id: 89,
date: 2023-10-01T11:57:35.945Z,
customerId: '65195ead9693271664edc258',
items: [
{
id: 177,
orderId: 89,
productId: '65195ead9693271664edc25c',
product: {
name: 'Samsung Galaxy S21',
price: 1199,
tags: [ 'Mobile phone', 'Samsung' ]
},
quantity: 2
},
{
id: 178,
orderId: 89,
productId: '65195ead9693271664edc25d',
product: {
name: 'iPhone 13 Pro Max',
price: 1099,
tags: [ 'Mobile phone', 'Apple' ]
},
quantity: 1
}
],
status: 'completed'
}
]
In this example, the product entity has multiple tags, and a tag has been assigned to multiple products, so it's a many-to-many relation which is covered with the product-tags
service/table.
Please note that in this example the
products
andtags
services use MongoDB database whileproduct-tags
uses PostgreSQL, but it doesn't matter because we use Moleculer DB populate feature to cover relations.
We create one-to-many relations in product-tags
service referencing to products
and tags
entities:
module.exports = {
name: "product-tags",
settings: {
fields: [
// ...
"product",
"tag",
// ...
],
// Populates for relations
populates: {
// The `productId` field references to a product.
product: {
field: "productId",
action: "products.get",
params: {
fields: ["name", "price"]
}
},
// The `orderId` field references to an order.
tag: {
field: "tagId",
action: "tags.get",
params: {
fields: ["id", "name"]
}
}
}
}
};
In products
service, we define a tags
populate which collects the assigned tag names:
module.exports = {
name: "products",
settings: {
fields: [
// ...
"tags",
// ...
],
// Populates for relations
populates: {
// ...
async tags(ids, products, rule, ctx) {
await Promise.all(products.map(async prd => {
const res = await ctx.call("product-tags.find", {
query: {
productId: prd.id
},
// Second level populating for tag entity
populate: ["tag"]
});
prd.tags = res.map(item => item.tag.name);
}));
},
// ...
}
}
};
And if necessary, we can define a products
populate in tags
service in order to collect products by tags:
module.exports = {
name: "tags",
settings: {
fields: [
// ...
"products",
// ...
],
// Populates for relations
populates: {
// ...
async products(ids, tags, rule, ctx) {
await Promise.all(tags.map(async prd => {
const res = await ctx.call("product-tags.find", {
query: {
tagId: prd.id
},
populate: ["product"]
});
prd.products = res.map(item => item.product);
}));
},
// ...
}
}
};
await this.broker.call("tags.get", { id: "...", populate: ["products"] })
{
id: '65195ead9693271664edc262',
name: 'Samsung',
products: [
{ name: 'Samsung Galaxy S21', price: 1199 },
{ name: 'Samsung Galaxy S23', price: 1669 }
]
}
In this example, there is no exact one-to-one relation demo because you can cover it with the first one-to-many relation.
With the populate feature, you can create virtual fields as well. Here are some examples:
We create an orderCount
virtual field which calculates the number of orders by a customer:
module.exports = {
name: "customers",
settings: {
fields: [
// ...
"orderCount",
// ...
],
// Populates for relations
populates: {
// ...
/* It's a virtual field, we calculate the value */
async orderCount(ids, customers, rule, ctx) {
await Promise.all(customers.map(async cus => {
const orders = await ctx.call("orders.find", {
query: {
customerId: cus.id
}
});
cus.orderCount = orders.length;
}));
}
// ...
}
}
};
await this.broker.call("customers.get", { id: "...", populate: ["orderCount"] })
{
id: '651963189693271664edc267',
name: 'Jane Doe',
email: '[email protected]',
orderCount: 1,
active: false
}
We create a totalPrice
virtual field which calculates the total price of an order. The price
comes from the product
entity of every order item and is multiplied by the quantity
in the order item:
module.exports = {
name: "orders",
settings: {
fields: [
// ...
"totalPrice",
// ...
],
// Populates for relations
populates: {
// ...
/* It's a virtual field, we calculate the value */
async totalPrice(ids, orders, rule, ctx) {
await Promise.all(orders.map(async order => {
const items = await ctx.call("orderItems.find", {
query: {
orderId: order.id
},
populate: ["product"]
});
order.totalPrice = items.reduce((a, b) => a + b.product.price * b.quantity, 0);
}));
},
// ...
}
}
};
await this.broker.call("orders.find", { populate: ["totalPrice", "items"] });
[
{
id: 97,
date: 2023-10-01T12:20:38.631Z,
customerId: '651964139693271664edc274',
items: [
{
id: 193,
orderId: 97,
productId: '651964149693271664edc278',
product: {
name: 'Samsung Galaxy S21',
price: 1199,
tags: [ 'Mobile phone', 'Samsung' ]
},
quantity: 2
},
{
id: 194,
orderId: 97,
productId: '651964149693271664edc279',
product: {
name: 'iPhone 13 Pro Max',
price: 1099,
tags: [ 'Mobile phone', 'Apple' ]
},
quantity: 1
}
],
totalPrice: 3497,
status: 'completed'
}
]