Create a Smart relationship

What is a Smart Relationship?

Sometimes, you want to create a virtual relationship between two set of data that does not exist in your database. A concrete example could be creating a relationship between two collections available in two different databases. Creating a Smart Relationship allows you to customize with code how your collections are linked together.

Create a BelongsTo Smart Relationship

On the Live Demo example, we have an order which belongsTo a customer which belongsTo a delivery address. We’ve created here a BelongsTo Smart Relationship that acts like a shortcut between the order and the delivery address.
A BelongsTo Smart Relationship is created like a Smart Field with the reference option to indicate on which collection the Smart Relationship points to. You will also need to code the logic of the search query.
SQL
Mongodb
Rails
Django
/forest/orders.js
1
const { collection } = require('forest-express-sequelize');
2
const models = require('../models');
3
4
collection('orders', {
5
fields: [{
6
field: 'delivery_address',
7
type: 'String',
8
reference: 'addresses.id',
9
get: function (order) {
10
return models.addresses
11
.findAll({
12
include: [{
13
model: models.customers,
14
where: { id: order.customer_id },
15
include: [{
16
model: models.orders,
17
where: { ref: order.ref }
18
}]
19
}],
20
})
21
.then((addresses) => {
22
if (addresses) { return addresses[0]; }
23
});
24
}
25
}]
26
});
Copied!
/forest/orders.js
1
const { collection } = require('forest-express-mongoose');
2
const Address = require('../models/addresses');
3
4
collection('Order', {
5
fields: [{
6
field: 'delivery_address',
7
type: 'String',
8
reference: 'Address._id',
9
get: function (order) {
10
return Address
11
.aggregate([
12
{
13
$lookup:
14
{
15
from: 'orders',
16
localField: 'customer_id',
17
foreignField: 'customer_id',
18
as: 'orders_docs'
19
}
20
},
21
{
22
$match:
23
{
24
'orders_docs._id': order._id
25
}
26
}
27
])
28
.then((addresses) => {
29
if (addresses) { return addresses[0]._id; }
30
});
31
}
32
}]
33
});
Copied!
lib/forest_liana/collections/order.rb
1
class Forest::Order
2
include ForestLiana::Collection
3
4
collection :Order
5
6
search_delivery_address = lambda do |query, search|
7
8
query.joins(customer: :address).or(Order.joins(customer: :address).where("addresses.country ILIKE ?", "%#{search}%"))
9
10
end
11
12
belongs_to :delivery_address, reference: 'Address.id', search: search_delivery_address do
13
object.customer.address
14
end
15
end
Copied!
app/forest/order.py
1
from django_forest.utils.collection import Collection
2
from app.models import Order, Address
3
4
5
class OrderForest(Collection):
6
def load(self):
7
self.fields = [
8
{
9
'field': 'delivery_address',
10
'reference': 'app_addresses.id',
11
'type': 'String',
12
'get': self.get_subject,
13
}
14
]
15
16
def get_delivery_address(self, obj):
17
queryset = Address.objects.filter(customer__in=(obj.customer_id,))
18
if len(queryset):
19
return queryset[0]
20
21
Collection.register(OrderForest, Order)
Copied!

Create a HasMany Smart Relationship

On the Live Demo example, we have a product hasMany orders and an order belongsTo customer. We’ve created a Smart Relationship that acts like a shortcut: product hasMany customers.
A HasMany Smart Relationship is created like a Smart Field with the reference option to indicates on which collection the Smart Relationship points to.
SQL
Mongodb
Rails
Django
/forest/products.js
1
const { collection } = require('forest-express-sequelize');
2
3
collection('products', {
4
fields: [{
5
field: 'buyers',
6
type: ['String'],
7
reference: 'customers.id'
8
}]
9
});
Copied!
/forest/products.js
1
const { collection } = require('forest-express-mongoose');
2
3
collection('products', {
4
fields: [{
5
field: 'buyers',
6
type: ['String'],
7
reference: 'Customer._id'
8
}]
9
});
Copied!
lib/forest_liana/collections/product.rb
1
class Forest::Product
2
include ForestLiana::Collection
3
4
collection :Product
5
6
has_many :buyers, type: ['String'], reference: 'Customer.id'
7
end
Copied!
app/forest/product.py
1
from django_forest.utils.collection import Collection
2
from app.models import Product
3
4
5
class ProductForest(Collection):
6
def load(self):
7
self.fields = [
8
{
9
'field': 'buyers',
10
'reference': 'app_customer.id',
11
'type': ['String'],
12
}
13
]
14
15
16
Collection.register(ProductForest, Product)
Copied!
SQL
Mongodb
Rails
Django
Upon browsing, an API call is triggered when accessing the data of the HasMany relationships in order to fetch them asynchronously. In the following example, the API call is a GET on /products/:product_id/relationships/buyers.

Option 1: using Sequelize ORM

We’ll use the findAll and count methods provided by Sequelize to find and count all customers who bought the current product (buyers).
Then, you should handle pagination in order to avoid performance issue. The API call has a query string available which gives you all the necessary parameters you need to enable pagination.
Finally, you don’t have to serialize the data yourself. The Forest Liana already knows how to serialize your collection (customers in this example). You can access to the serializer through the recordsGetter.serialize function.
/routes/products.js
1
const express = require('express');
2
const { PermissionMiddlewareCreator, RecordSerializer } = require('forest-express-sequelize');
3
const { products, customers, orders} = require('../models');
4
5
const router = express.Router();
6
const permissionMiddlewareCreator = new PermissionMiddlewareCreator('products');
7
8
router.get('/products/:product_id/relationships/buyers', (request, response, next) => {
9
const productId = request.params.product_id;
10
const limit = parseInt(request.query.page.size, 10) || 20;
11
const offset = (parseInt(request.query.page.number, 10) - 1) * limit;
12
const include = [{
13
model: orders,
14
as: 'orders',
15
where: { product_id: productId },
16
}];
17
18
// find the customers for the requested page and page size
19
const findAll = customers.findAll({
20
include,
21
offset,
22
limit,
23
});
24
25
// count all customers for pagination
26
const count = customers.count({ include });
27
28
// resolve the two promises and serialize the response
29
const serializer = new RecordSerializer(customers);
30
Promise.all([findAll, count])
31
.then(([customersFound, customersCount]) =>
32
serializer.serialize(customersFound, { count: customersCount }))
33
.then((recordsSerialized) => response.send(recordsSerialized))
34
.catch(next);
35
});
Copied!

Option2: using raw SQL

We’ll use raw SQL query and Sequelize to count and find all customers who bought the current product (buyers).
Then, you should handle pagination in order to avoid performance issue. The API call has a query string available which gives you all the necessary parameters you need to enable pagination.
Finally, you don’t have to serialize the data yourself. The Forest Liana already knows how to serialize your collection (customers in this example). You can access to the serializer through the recordsGetter.serialize function.
/routes/products.js
1
const express = require('express');
2
const router = express.Router();
3
const models = require('../models');
4
5
router.get('/products/:product_id/relationships/buyers', (req, res, next) => {
6
let limit = parseInt(req.query.page.size) || 10;
7
let offset = (parseInt(req.query.page.number) - 1) * limit;
8
let queryType = models.sequelize.QueryTypes.SELECT;
9
10
let countQuery = `
11
SELECT COUNT(*)
12
FROM customers
13
JOIN orders ON orders.customer_id = customers.id
14
JOIN products ON orders.product_id = products.id
15
WHERE product_id = ${req.params.product_id};
16
`;
17
18
let dataQuery = `
19
SELECT customers.*
20
FROM customers
21
JOIN orders ON orders.customer_id = customers.id
22
JOIN products ON orders.product_id = products.id
23
WHERE product_id = ${req.params.product_id}
24
LIMIT ${limit}
25
OFFSET ${offset}
26
`;
27
28
const serializer = new RecordsSerializer(customers);
29
return Promise
30
.all([
31
models.sequelize.query(countQuery, { type: queryType }),
32
models.sequelize.query(dataQuery, { type: queryType })
33
])
34
.then(([count, customers]) => serializer.serialize(customers, { count: count[0].count }))
35
.then((customers) => res.send(customers))
36
.catch((err) => next(err));
37
});
38
39
module.exports = router;
Copied!
If your primary key column name (customer_id) is different than the model field name (customerId), you must alias the primary key column with the name of the model field in the dataQuery. Ex: SELECT customers.*, customers.customer_id AS “customerId”
Upon browsing, an API call is triggered when accessing the data of the HasMany relationships in order to fetch them asynchronously. In the following example, the API call is a GET on /Product/:product_id/relationships/buyers.
We use the $lookup operator of the aggregate pipeline. Since there's a many-to-many relationship between Product and Customer, the $lookup operator needs to look into orders which is an array we have to flatten first using $unwind.
Finally, you don’t have to serialize the data yourself. The Forest Liana already knows how to serialize your collection (Customer in this example). You can access to the serializer through the Liana.ResourceSerializer object.
/forest/products.js
1
const { collection } = require('forest-express-mongoose');
2
3
collection('products', {
4
fields: [{
5
field: 'buyers',
6
type: ['String'],
7
reference: 'Customer._id'
8
}]
9
});
Copied!
/routes/products.js
1
const P = require('bluebird');
2
const express = require('express');
3
const router = express.Router();
4
const Liana = require('forest-express-mongoose');
5
const { Customers } = require('../models');
6
const mongoose = require('mongoose');
7
8
router.get('/Product/:product_id/relationships/buyers', (req, res, next) => {
9
let limit = parseInt(req.query.page.size) || 10;
10
let offset = (parseInt(req.query.page.number) - 1) * limit;
11
12
let countQuery = Customers.aggregate([
13
{
14
$lookup:
15
{
16
from: 'orders',
17
localField: 'orders',
18
foreignField: '_id',
19
as: 'orders_docs'
20
}
21
},
22
{
23
$unwind: "$orders_docs"
24
},
25
{
26
$lookup:
27
{
28
from: 'products',
29
localField: 'orders_docs._id',
30
foreignField: 'orders',
31
as: 'products_docs'
32
}
33
},
34
{
35
$match:
36
{
37
'products_docs._id': mongoose.Types.ObjectId(req.params.product_id)
38
}
39
},
40
{
41
$count: "products_docs"
42
}
43
]);
44
45
let dataQuery = Customers.aggregate([
46
{
47
$lookup:
48
{
49
from: 'orders',
50
localField: 'orders',
51
foreignField: '_id',
52
as: 'orders_docs'
53
}
54
},
55
{
56
$unwind: "$orders_docs"
57
},
58
{
59
$lookup:
60
{
61
from: 'products',
62
localField: 'orders_docs._id',
63
foreignField: 'orders',
64
as: 'products_docs'
65
}
66
},
67
{
68
$match:
69
{
70
'products_docs._id': mongoose.Types.ObjectId(req.params.product_id)
71
}
72
}
73
]);
74
75
return P
76
.all([
77
countQuery,
78
dataQuery
79
])
80
.spread((count, customers) => {
81
const serializer = new Liana.RecordSerializer(Customers);
82
return serializer.serialize(customers, { count: count.orders_count });
83
})
84
.then((products) => {
85
res.send(products);
86
})
87
.catch((err) => next(err));
88
});
89
90
module.exports = router;
Copied!
Upon browsing, an API call is triggered when accessing the data of the HasMany relationships in order to fetch them asynchronously. In the following example, the API call is a GET on /Product/:product_id/relationships/buyers.
We’ve built the right SQL query using Active Record to count and find all customers who bought the current product.
Then, you should handle pagination in order to avoid performance issue. The API call has a querystring available which gives you all the necessary parameters you need to enable pagination.
Finally, you don’t have to serialize the data yourself. The Forest Liana already knows how to serialize your collection (Customer in this example). You can access to the serializer through the serialize_models() function.
1
Rails.application.routes.draw do
2
# MUST be declared before the mount ForestLiana::Engine.
3
namespace :forest do
4
get '/Product/:product_id/relationships/buyers' => 'orders#buyers'
5
end
6
7
mount ForestLiana::Engine => '/forest'
8
end
Copied!
1
class Forest::ProductsController < ForestLiana::ApplicationController
2
def buyers
3
limit = params['page']['size'].to_i
4
offset = (params['page']['number'].to_i - 1) * limit
5
6
orders = Product.find(params['product_id']).orders
7
customers = orders.limit(limit).offset(offset).map(&:customer)
8
count = orders.count
9
10
render json: serialize_models(customers, include: ['address'], meta {count: count})
11
end
12
end
Copied!
Upon browsing, an API call is triggered when accessing the data of the HasMany relationships in order to fetch them asynchronously. In the following example, the API call is a GET on /app_product/:product_pk/relationships/buyers. You will have to declare this route in your app urls.py file
app/urls.py
1
from django.urls import path
2
from django.views.decorators.csrf import csrf_exempt
3
4
from . import views
5
6
app_name = 'app'
7
urlpatterns = [
8
path('/app_product/<pk>/relationships/buyers', views.BuyersView.as_view(), name='product-buyers'),
9
]
Copied!
Then create the pertained view
app/views.py
1
from django.http import JsonResponse
2
from django.views import generic
3
4
from django_forest.resources.utils.queryset import PaginationMixin
5
from django_forest.utils.schema.json_api_schema import JsonApiSchema
6
7
class BuyersView(PaginationMixin, generic.ListView):
8
9
def get(self, request, pk, *args, **kwargs):
10
params = request.GET.dict()
11
12
# queryset
13
queryset = Customer.objects.filter(order__product_id=pk).distinct()
14
15
# pagination
16
queryset = self.get_pagination(params, queryset)
17
18
# json api serializer
19
Schema = JsonApiSchema.get('app_customer')
20
data = Schema().dump(queryset, many=True)
21
22
return JsonResponse(data, safe=False)
Copied!
We’ve built the right SQL query using Django ORM to find all customers who bought the current product.
Then, you should handle pagination in order to avoid performance issue. The API call has a querystring available which gives you all the necessary parameters you need to enable pagination.
Finally, you don’t have to serialize the data yourself. The Forest Liana already knows how to serialize your collection (Customer in this example, with the table name app_customer). You can access to the serializer through the Schema().dump function (using marshmallow-jsonapi internally).
Last modified 4d ago