Repository Service Deep Dive
In this article, we do a deep dive on the repository service. We will start with an overview of repository service, followed by an examination of the various components of repository service. We will then present a set of simple and complex examples of repository service.
We assume that you have had some hands-on experience with Service Builder, the development tool for data access service, and with the data access service. If not, please take a look at these tutorial first:
Get Started with Service Builder
Get Started with Data Access Service
Overview
Use
Repository service is for CRUD operations of aggregate objects. It aims to provide the developer the flexibility to create efficient repository for any aggregate that is needed by the application.
Components
Conceptually, the repository service is composed of the following components:
- object
-
read
-
input
- query
- input bindings
-
output bindings
-
write
-
table bindings
- column bindings
The object component is a JSON object specifying the shape and fields of the aggregate object.
The read component specifies a dynamic query service to retrieve the aggregate objects. Its output is always an array of the aggregate objects.
The write component comprises table bindings and column bindings. The table bindings specifies the tables to write and maps the tables to the root and child structures of the aggregate object. The column bindings map the columns of each table to the data fields of the object or structure.
The read and write components are specified separately, and thus changing a potential bi-directional mapping into two uni-direction mappings. This simplifies the difficult object-relational mapping problem, and supports asymmetric read-write as we will see in the example below.
Operations
Unlike the single-operation SQL and query services, the repository service supports a number of read and write operations on the aggregate object, including
- read, for retrieving object
- create, for creating object
- update, for updating object in their entirety, equivalent to replacement
- delete, for removing object
- save, for updating or creating (if not exist) object, and
- merge, for merging changes into object
Development
Service Builder provides a streamlined process for developing the repository service in a few simple steps:
- create the service
- specify the object in JSON
- develop the SQL query for read
- generate, review and edit, if needed, the input and output bindings for read, and
- generate, review and edit, if needed, the table and column bindings for write
- generate, edit and run the tests for the service
For read
, the focus is on developing the SQL query to return the data set for populating the object. For write
, the focus is on the column bindings to add record(s) for each table. As the SQL query for read is also responsible for providing information needed to generate the table and column bindings, it must follow a number of rules for repository query.
As such, it is important to create a SQL development environment in VS Code with the various database extensions available, so that we have the visibility of database in VS Code and have the help of SQL syntax linting, SQL intellisense, SQL test, etc. Otherwise, we will need to compose and test the SQL in a specialized SQL editor, such as Oracle SQL developer, and then copy and paste them into the query.sql
file.
As soon as the SQL service is completed, we should try to deploy it into the remote workspace, which helps validate the service. We can then fix the issue, if any, and repeat the process until it is clean. Thereafter, we can proceed to adding the test.
For repository service, at minimum, we will need one test for each operation. For read, we are expected to add one test for each read scenarios. For example, we may have one test for readAll
and one test for readById
.
The test for write operations may be run with or without a commit. A rollback will be performed at the end of the test if it is run without commit.
Deployment
The repository service is deployed on the data access server as a set of HTTP APIs, one for each operation. At development time, it is deployed into the remote workspace as it is developed. At runtime, it is deployed as part of the data access application.
Service Calls
Since the repository service is deployed as a set of HTTP APIs, it may be invoked by a HTTP request for read or write, like
POST: https://{baseUrl}/application/module/service/operation
Content Type: application/json
{
"json data": 123
}
The HTTP method is always POST
. The baseUrl
depends on the workspace or runtime instance. The operation is read
, create
, update
, delete
, save
or merge
. The body of the request is the input for read or the object for write. The delete
operation only needs to include the key field(s) in the object. The input for the write operations may be an array of the objects. In this case, the objects will be processed in a single transaction.
Anatomy
Lets take a look at the repository service through an example: Order
. The service is to provide CRUD operations for the Order
object.
Files
Upon creation, we will get a set of service files as below:
- Order
- service.json
- object.json
- read
- input.json
- query.sql
- input-bindings.json
- output-bindings.json
- write
- tables.json
- {schema}.{table}.{alias}.columns.json
The service.json
files is a service descriptor generated by Service Builder and is not supposed to be modified. The rest of files are the various components of repository service and are what are to be developed by the developer.
Object
The object define what the target of the repository service, and the problem for the developer to solve.
For our example, we have
{
"orderNumber": 1,
"orderDate": "2020-12-10T00:00:00.000",
"customerNumber": 103,
"customerName": "John",
"requiredDate": "2020-12-10T00:00:00.000",
"shippedDate": "2020-12-10T00:00:00.000",
"status": "shipped",
"comments": "shipped on time",
"total": "345.60",
"lines": [{
"orderLineNumber": 10,
"productCode": "S12_1099",
"productName": "1968 Ford Mustang",
"qty": 2,
"price": 35.45,
"subtotal": 70.90
}]
}
Read
Input
The input defines the parameters that are to be used for read. As read
is a dynamic query, not all parameters defined in the input need to be supplied in the service request, depending on the read request.
For our example, we have:
{
"orderNumber": 1,
"customerNumber": 3,
"startDate": "2020-12-01T00:00:00.000",
"endDate": "2020-12-31T00:00:00.000"
}
It is to support readByOrderNumber
, readByCustomerNumber
and readByOrderDateRange
. Note that the startDate
and endDate
are not from the Order
object. The separate input
for read
operation provides a great amount of flexibility.
Query
This SQL query is responsible for retrieving the data set for populating all data fields of the object. It is the developer's answer to the repository read problem.
For our example, we have
select o.orderNumber, o.orderDate,
o.customerNumber, _c.customerName,
o.requiredDate, o.shippedDate, o.status, o.comments,
od.orderLineNumber, od.productCode, _p.productName,
od.quantityOrdered as qty, od.priceEach, od.quantityOrdered*od.priceEach as subtotal,
(select sum(quantityOrdered*priceEach) from orderdetails
where orderNumber = o.orderNumber) as total
from orders o
join customers _c on o.customerNumber = _c.customerNumber
left join orderdetails od on od.orderNumber = o.orderNumber
left join products _p on _p.productCode = od.productCode
where ( 0 = 1
or :orderNumber is not null
or :customerNumber is not null
or :startDate is not null
or :endDate is not null
)
and o.orderNumber = :orderNumber
and o.customerNumber = :customerNumber
and o.orderDate between :startDate and :endDate
This read query is dynamic by design. Therefore, if we invoke the read operation by passing an orderNumber
, we will get the order identified by the orderNumber; if we the read operation by passing a customerNumber
, we will get the list of orders associated with the customer; if we invoke the read operation without passing any parameter, we will get empty list from this query. The WHERE
clause of the read query shall be formulated to support all query needs for the object.
From this query, we see that the data for populating the Order
object is read not only from the orders
and order_lines
tables, but also from the customers
and products
tables. However, when writing the Order
object back to the database, we will only write the data into the orders
and order_lines
tables. The total
and subtotal
fields within the Order
object are also read-only fields summed up from the order details, and are not to be written back to the database. This is the so-called read-write asymmetry, and is supported by the repository service by design.
The customers
and products
tables here are called reference tables, as data is only read from them. From DDD (domain driven design) perspective, they are not part of the domain of concern. The orders
table is called root table, as it is mapped to the root object. The order_lines
table is called child table as it is mapped to a child structure of the root object. The write operations write data to the root and child tables.
In order for Service Builder to be able to generate proper table and column bindings from the read components, there are a few rules that the repository query must follow:
- The query must be in ANSI format, with the root table appearing in the
FROM
clause and the child and reference tables in theJOIN
clauses; - All tables must be aliased and all columns must be prefixed with the table alias, like
o.orderNumber
; - The alias for the reference tables should start with an underscore, like
_c
and_p
in above query for thecustomers
andproducts
tables, respectively. - The child tables must be joined in the order that the records are to be inserted when creating the object.
Input and Output Bindings
The input bindings indicate the source of data for the query parameters. The output bindings provide the critical information to construct the aggregate object(s) from the relational data set returned by the query.
For our example, we have
Input bindings:
parameter | field | |
---|---|---|
0 | orderNumber | .orderNumber |
1 | endDate | .endDate |
2 | customerNumber | .customerNumber |
3 | startDate | .startDate |
Output Bindings:
field | column | |
---|---|---|
0 | .orderNumber | orderNumber |
1 | .orderDate | orderDate |
2 | .customerNumber | customerNumber |
3 | .customerName | customerName |
4 | .requiredDate | requiredDate |
5 | .shippedDate | shippedDate |
6 | .status | status |
7 | .comments | comments |
8 | .total | total |
9 | .lines..orderLineNumber | orderLineNumber |
10 | .lines..productCode | productCode |
11 | .lines..productName | productName |
12 | .lines..qty | qty |
13 | .lines..price | priceEach |
14 | .lines..subtotal | subtotal |
Like the query service, the input and output bindings are not expected to be hand-coded from scratch, but generated with Service Builder at first, and then reviewed and edited, if necessary, by the developer.
Write
Table Bindings
The table bindings specify the tables to write and map them to the root and child structures of the aggregate object.
For our example, we have
[
{
"name": "orders",
"alias": "o",
"object": ".",
"rootTable": true,
"mainTable": true,
"operationIndicator": null,
"columns": "./orders.o.columns.json"
},
{
"name": "orderdetails",
"alias": "od",
"object": ".lines..",
"rootTable": false,
"mainTable": true,
"operationIndicator": null,
"columns": "./orderdetails.od.columns.json"
}
]
There are two entries, for the root orders
table and the child orderDetails
table, respectively. The table bindings component is generated from the read query, and is not to be edited except for the mainTable
and operationIndicator
attributes that we do not discuss here.
Column Bindings
There is one column bindings component for each mapped table. The column bindings specify how the record should be inserted, updated and deleted for the relevant write operations.
For the orders
table, we have
position | column | field | key | autoGenerate | inputField | insertValue | updateValue | version | softDelete | dataType | notNull | keyEligible | versionEligible | softDeleteEligible |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | orderNumber | .orderNumber | true | false | .orderNumber | false | false | number | true | true | true | false | ||
2 | orderDate | .orderDate | false | false | CURRENT_TIMESTAMP | false | false | datetime | true | false | false | false | ||
3 | requiredDate | .requiredDate | false | false | false | false | datetime | true | false | false | false | |||
4 | shippedDate | .shippedDate | false | false | false | false | datetime | false | false | false | false | |||
5 | status | .status | false | false | false | false | string | true | false | false | true | |||
6 | comments | .comments | false | false | 'no comments' | false | false | string | false | false | false | false | ||
7 | customerNumber | .customerNumber | false | false | false | false | number | true | false | true | false |
Here is a brief explanation of the various attributes in the column bindings:
- column position in the table
- column: the column name
- field: the data field mapped to the column
- key: whether the column is part of the key
- autoGenerate: whether an auto generated column
- input field: path to input data field, controls whether to return updated object
- insertValue: SQL expression to generate value for insert
- updateValue: SQL expression to generate value for update
- version: whether a version control column
- softDelete: whether a column for soft delete
- dataType: generic data type of the column, for reference
- notNull: whether a not null column, for reference
- keyEligible: whether eligible to be a key column, for reference
- versionEligible: whether eligible to be a version control column, for reference
- softDeleteEligible: whether whether eligible to be a column for soft delete, for reference
These attributes are extracted from the table meta data. The attribute we may edit include:
- key: if primary key is defined for the table
- input field: if not populated bu we want to return updated object for write operations
- inputValue: if we want to use this value for insert, regardless whether the column is mapped to an object field
- updateValue if you want to use this value for update, regardless whether the column is mapped to an object field
- version: to true, if we want to enable version control on the table
- softDelete: to true, if we want to enable soft delete for the table
For INSERT, autoGenerate
takes precedence over insert value
; insert value
takes precedence over mapped field
. If neither is available, the column will be excluded from the INSERT statement, and the value will be null
or the database default. For UPDATE, update value
takes precedence over mapped field
. If neither is available, the column will be excluded from the UPDATE statement, and the column will not be updated.
The insert value
and update value
shall be a SQL expression, like the insert value
: CURRENT_TIMESTAMP
and 'no comments'
for columns orderDate
and comments
, respectively, in our example.
Examples
The examples in this tutorial demonstrates what we can do with repository and how we could do it. The sample MySQL database used for the examples is the classicmodels
. The source code for these example repository services can be found here.
For the seek of brevity, the input and output bindings are omitted from presentations of all examples.
Example #1 - Office
Problem
Repository for Office
. The simplest repository service for a simple object mapped to a single table. It may be directly generated from the table.
Object
{
"officeCode": "10",
"address": "100 Market Street",
"phone": "+1 650 219 4782",
"city": "San Francisco",
"state": "CA",
"country": "USA",
"postalCode": "94080",
"territory": "NA"
}
Read
Input
Query
SELECT o.officeCode, o.addressLine1 as address, o.phone,
o.city, o.state, o.country, o.postalCode, o.territory
FROM offices o
WHERE 1 = 1
AND o.officeCode = :officeCode
AND o.country = :country
This service supports readByOfficeCode
, readByCountry
and readAllOffices
.
Write
Table Bindings
[
{
"name": "offices",
"alias": "o",
"object": ".",
"rootTable": true,
"mainTable": true,
"columns": "./offices.columns.json"
}
]
Column Bindings
position | column | field | key | autoGenerate | inputField | insertValue | updateValue | version | softDelete | dataType | notNull | keyEligible | versionEligible | softDeleteEligible |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | officeCode | .officeCode | true | false | .officeCode | false | false | string | true | true | false | true | ||
2 | city | .city | false | false | false | false | string | true | false | false | true | |||
3 | phone | .phone | false | false | false | false | string | true | false | false | true | |||
4 | addressLine1 | .address | false | false | false | false | string | true | false | false | true | |||
5 | addressLine2 | false | false | false | false | string | false | false | false | true | ||||
6 | state | .state | false | false | false | false | string | false | false | false | true | |||
7 | country | .country | false | false | false | false | string | true | false | false | true | |||
8 | postalCode | .postalCode | false | false | false | false | string | true | false | false | true | |||
9 | territory | .territory | false | false | false | false | string | true | false | false | true |
Note that the addressLine2
column is not mapped to any data field.
Example #2 - Customer
Problem
Repository for Customer
. A repository service for a structured object mapped to a single table.
Object
{
"customerNumber": 1,
"customerName": "Land of Toys Inc.",
"contact": {
"firstName": "Joe",
"LastName": "Williamson",
"phone": "(171) 555-2282"
},
"address": {
"addressLine1": "5557 North Pendale Street",
"addressLine2": "",
"city": "NYC",
"state": "NY",
"country": "USA"
}
}
Read
Input
Query
select c.customerNumber, c.customerName,
c.contactLastName, c.contactFirstName, c.phone,
c.addressLine1, c.addressLine2, c.city, c.state,
c.country , c.postalCode
from customers c
where 1 = 1
and c.customerNumber = :customerNumber
and c.city = :city
and c.postalCode = :postalCode
This service supports readByCustomerNumber
, readByCity
, readByPostalCode
and readAllCustomers
.
Write
Table Bindings
[
{
"name": "customers",
"alias": "c",
"object": ".",
"rootTable": true,
"mainTable": true,
"operationIndicator": null,
"columns": "./customers.c.columns.json"
}
]
Column Bindings
position | column | field | key | autoGenerate | inputField | insertValue | updateValue | version | softDelete | dataType | notNull | keyEligible | versionEligible | softDeleteEligible |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | customerNumber | .customerNumber | true | false | .customerNumber | false | false | number | true | true | true | false | ||
2 | customerName | .customerName | false | false | false | false | string | true | false | false | true | |||
3 | contactLastName | .contact.LastName | false | false | false | false | string | true | false | false | true | |||
4 | contactFirstName | .contact.firstName | false | false | false | false | string | true | false | false | true | |||
5 | phone | .contact.phone | false | false | false | false | string | true | false | false | true | |||
6 | addressLine1 | .address.addressLine1 | false | false | false | false | string | true | false | false | true | |||
7 | addressLine2 | .address.addressLine2 | false | false | false | false | string | false | false | false | true | |||
8 | city | .address.city | false | false | false | false | string | true | false | false | true | |||
9 | state | .address.state | false | false | false | false | string | false | false | false | true | |||
10 | postalCode | false | false | false | false | string | false | false | false | true | ||||
11 | country | .address.country | false | false | false | false | string | true | false | false | true | |||
12 | salesRepEmployeeNumber | false | false | false | false | number | false | false | true | false | ||||
13 | creditLimit | false | false | false | false | number | false | false | false | false |
Example #3 - ProductLine
Problem
Repository for maintaining product line. A repository service for object with array structure.
Object
{
"productLine": "Classic Cars",
"description": "Land of Toys Inc.",
"image": "base64YmFzZTY0ZW5jb2RlZA==",
"products": [{
"productCode": "S12_1099",
"productName": "1968 Ford Mustang",
"productVendor": "Motor City Art Classics",
"productDescription": "Hood, doors and trunk all open.",
"productScale": "1:100",
"quantityInStock": 100,
"buyPrice": "15.45",
"MSRP": "46.00"
}]
}
Read
Input
Query
select pl.productLine, pl.textDescription, pl.image,
p.productCode, p.productName, p.productVendor, p.productDescription,
p.productScale, p.quantityInStock, p.buyPrice, p.MSRP
from productlines pl
left join products p on p.productLine = pl.productLine
where 1 = 1
and pl.productLine = :productLine
and pl.productLine in (select productLine from products where productName = :productName)
This service supports readAllProductLines
, readByProductLineName
, and readProductLinesWithProductName
that returns product lines including product with a given name. From this example, it is seen that we have a fair amount of flexibility in formulating the WHERE
clause of the query, in order to satisfy the various query needs for the object.
Write
Table Bindings
[
{
"name": "productlines",
"alias": "pl",
"object": ".",
"rootTable": true,
"mainTable": true,
"operationIndicator": null,
"columns": "./productlines.pl.columns.json"
},
{
"name": "products",
"alias": "p",
"object": ".products..",
"rootTable": false,
"mainTable": true,
"operationIndicator": null,
"columns": "./products.p.columns.json"
}
]
Column Bindings - productlines:
position | column | field | key | autoGenerate | inputField | insertValue | updateValue | version | softDelete | dataType | notNull | keyEligible | versionEligible | softDeleteEligible |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | productLine | .productLine | true | false | .productLine | false | false | string | true | true | false | true | ||
2 | textDescription | .description | false | false | false | false | string | false | false | false | true | |||
3 | htmlDescription | false | false | false | false | string | false | false | false | false | ||||
4 | image | .image | false | false | false | false | binary | false | false | false | false |
Column Bindings - products:
position | column | field | key | autoGenerate | inputField | insertValue | updateValue | version | softDelete | dataType | notNull | keyEligible | versionEligible | softDeleteEligible |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | productCode | .products..productCode | true | false | .products..productCode | false | false | string | true | true | false | true | ||
2 | productName | .products..productName | false | false | false | false | string | true | false | false | true | |||
3 | productLine | .productLine | false | false | false | false | string | true | false | false | true | |||
4 | productScale | .products..productScale | false | false | false | false | string | true | false | false | true | |||
5 | productVendor | .products..productVendor | false | false | false | false | string | true | false | false | true | |||
6 | productDescription | .products..productDescription | false | false | false | false | string | true | false | false | false | |||
7 | quantityInStock | .products..quantityInStock | false | false | false | false | number | true | false | true | false | |||
8 | buyPrice | .products..buyPrice | false | false | false | false | number | true | false | false | false | |||
9 | MSRP | .products..MSRP | false | false | false | false | number | true | false | false | false |
Example #4 - Order
Problem
Repository for Order
. A repository service for object with array structure, featuring read-write asymmetry. This is the example used in the Anatomy section and is relisted here.
Object
{
"orderNumber": 1,
"orderDate": "2020-12-10T00:00:00.000",
"customerNumber": 103,
"customerName": "John",
"requiredDate": "2020-12-10T00:00:00.000",
"shippedDate": "2020-12-10T00:00:00.000",
"status": "shipped",
"comments": "shipped on time",
"total": "345.60",
"lines": [{
"orderLineNumber": 10,
"productCode": "S12_1099",
"productName": "1968 Ford Mustang",
"qty": 2,
"price": 35.45,
"subtotal": 70.90
}]
}
Read
Input
{
"orderNumber": 1,
"customerNumber": 103,
"startDate": "2004-12-01T00:00:00.000",
"endDate": "2004-12-31T00:00:00.000"
}
Query
select o.orderNumber, o.orderDate,
o.customerNumber, _c.customerName,
o.requiredDate, o.shippedDate, o.status, o.comments,
od.orderLineNumber, od.productCode, _p.productName,
od.quantityOrdered as qty, od.priceEach, od.quantityOrdered*od.priceEach as subtotal,
(select sum(quantityOrdered*priceEach) from orderdetails
where orderNumber = o.orderNumber) as total
from orders o
join customers _c on o.customerNumber = _c.customerNumber
left join orderdetails od on od.orderNumber = o.orderNumber
left join products _p on _p.productCode = od.productCode
where ( 0 = 1
or :orderNumber is not null
or :customerNumber is not null
or :startDate is not null
or :endDate is not null
)
and o.orderNumber = :orderNumber
and o.customerNumber = :customerNumber
and o.orderDate between :startDate and :endDate
This service supports readByOrderNumber
, readByCustomerNumber
and readByOrderDateRange
, but does not support readAllOrders
. Point worth noting include:
- The
total
andsubtotal
column are calculated fields for read purpose only - The query parameters
startDate
andendDate
are not mapped to any table column.
Write
[
{
"name": "orders",
"alias": "o",
"object": ".",
"rootTable": true,
"mainTable": true,
"operationIndicator": null,
"columns": "./orders.o.columns.json"
},
{
"name": "orderdetails",
"alias": "od",
"object": ".lines..",
"rootTable": false,
"mainTable": true,
"operationIndicator": null,
"columns": "./orderdetails.od.columns.json"
}
]
Column Bindings - orders:
position | column | field | key | autoGenerate | inputField | insertValue | updateValue | version | softDelete | dataType | notNull | keyEligible | versionEligible | softDeleteEligible |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | orderNumber | .orderNumber | true | false | .orderNumber | false | false | number | true | true | true | false | ||
2 | orderDate | .orderDate | false | false | CURRENT_TIMESTAMP | false | false | datetime | true | false | false | false | ||
3 | requiredDate | .requiredDate | false | false | false | false | datetime | true | false | false | false | |||
4 | shippedDate | .shippedDate | false | false | false | false | datetime | false | false | false | false | |||
5 | status | .status | false | false | false | false | string | true | false | false | true | |||
6 | comments | .comments | false | false | 'no comments' | false | false | string | false | false | false | false | ||
7 | customerNumber | .customerNumber | false | false | false | false | number | true | false | true | false |
Column Bindings - orderdetails:
position | column | field | key | autoGenerate | inputField | insertValue | updateValue | version | softDelete | dataType | notNull | keyEligible | versionEligible | softDeleteEligible |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | orderNumber | .orderNumber | true | false | false | false | number | true | true | true | false | |||
2 | productCode | .lines..productCode | true | false | false | false | string | true | true | false | true | |||
3 | quantityOrdered | .lines..qty | false | false | false | false | number | true | false | true | false | |||
4 | priceEach | .lines..price | false | false | false | false | number | true | false | false | false | |||
5 | orderLineNumber | .lines..orderLineNumber | false | false | false | false | number | true | false | true | false |
Note that the orderdetails
table has a composite key in orderNumber
and productCode
.
Example #5 - OfficeAggregate
Problem
Repository for OfficeAggregate. A repository service for a complex object.
Object
{
"officeCode": "100",
"name": "San Francisco",
"address": {
"city": "San Francisco",
"phone": "312-243-0567",
"address": "123 Main st.",
"state": "CA",
"country": "USA",
"postalCode": "81000"
},
"manager": {
"employeeNumber": 1,
"firstName": "John",
"lastName": "Smith",
"jobTitle": "Office Manager",
"extension": "1234",
"email": ""
},
"employees": [{
"employeeNumber": 2,
"firstName": "John",
"lastName": "Smith",
"jobTitle": "Developer",
"extension": "1234",
"email": "",
"reportsTo": 1
}]
}
Read
Input
Query
select e.employeeNumber as eEmployeeNumber, e.lastName as eLastName,
e.firstName as eFirstName, e.jobTitle as eJobTitle, o.officeCode, o.city, o.phone, o.addressLine1,
o.state, o.country, o.postalCode,
e.employeeNumber as eEmployeeNumber, e.lastName as eLastName,
e.firstName as eFirstName, e.jobTitle as eJobTitle,
e.extension as eExtension, e.email as eEmail, e.reportsTo,
m.employeeNumber as mEmployeeNumber, m.lastName as mLastName,
m.firstName as mFirstName, m.jobTitle as mJobTitle,
m.extension as mExtension, m.email as mEmail
from offices o
left join employees m on m.officeCode = o.officeCode and m.jobTitle = 'office manager'
left join employees e on e.officeCode = o.officeCode and e.jobTitle != 'office manager'
where 1 = 1
and o.officeCode = :officeCode
Output Bindings
field | column |
---|---|
.officeCode | officeCode |
.name | city |
.address.city | city |
.address.state | state |
... |
Point worth noting:
- The
name
of office does not exist in the table or SQL query. We have it mapped to thecity
column to havecity
as the office name; - Column
city
is also mapped to thecity
field withinOfficeAggregate
.
Write
Table Bindings
name | alias | object | rootTable | mainTable | operationIndicator | columns |
---|---|---|---|---|---|---|
offices | o | . | true | true | ./offices.columns.json | |
employees | m | .manager. | false | false | ./employees.columns.json | |
employees | e | .employees.. | false | true | ./employees.columns.json |
Point worth noting:
- The
employees
table appeared twice in the table bindings, one for the"manager"
record and one for the"employees"
records. The two are differentiated by table aliases; - When the
Office
object is created in the database, there will be one record inserted into theoffices
table, one record inserted into theemployees
table for"manager"
, and multiple records inserted into theemployees
table for"employees"
.
Column Bindings
Office:
position | column | field | key | autoGenerate | inputField | insertValue | updateValue | version | softDelete | dataType | notNull | keyEligible | versionEligible | softDeleteEligible |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | officeCode | .officeCode | true | false | .officeCode | false | false | string | true | true | false | true | ||
2 | city | .address.city | false | false | false | false | string | true | false | false | true | |||
3 | phone | .address.phone | false | false | false | false | string | true | false | false | true | |||
4 | addressLine1 | .address.address | false | false | false | false | string | true | false | false | true | |||
5 | addressLine2 | false | false | false | false | string | false | false | false | true | ||||
6 | state | .address.state | false | false | false | false | string | false | false | false | true | |||
7 | country | .address.country | false | false | false | false | string | true | false | false | true | |||
8 | postalCode | .address.postalCode | false | false | false | false | string | true | false | false | true | |||
9 | territory | false | false | 'NA' | false | false | string | true | false | false | true |
Point worth noting:
- The
territory
column is aNOT NULL
column and is not mapped to any data field. We set ainsert value
for it to avoid INSERT error.
Manager:
position | column | field | key | autoGenerate | inputField | insertValue | updateValue | version | softDelete | dataType | notNull | keyEligible | versionEligible | softDeleteEligible |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | employeeNumber | .manager.employeeNumber | true | false | false | false | number | true | true | true | false | |||
2 | lastName | .manager.lastName | false | false | false | false | string | true | false | false | true | |||
3 | firstName | .manager.firstName | false | false | false | false | string | true | false | false | true | |||
4 | extension | .manager.extension | false | false | false | false | string | true | false | false | true | |||
5 | .manager.email | false | false | false | false | string | true | false | false | true | ||||
6 | officeCode | .officeCode | false | false | false | false | string | true | false | false | true | |||
7 | reportsTo | false | false | false | false | number | false | false | true | false | ||||
8 | jobTitle | .manager.jobTitle | false | false | 'office manager' | false | false | string | true | false | false | true |
Point worth noting:
- The
job title
for the"manager"
is"office manager"
. We set theinsert value
to be'office manager'
.
Employee:
position | column | field | key | autoGenerate | inputField | insertValue | updateValue | version | softDelete | dataType | notNull | keyEligible | versionEligible | softDeleteEligible |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | employeeNumber | .employees..employeeNumber | true | false | false | false | number | true | true | true | false | |||
2 | lastName | .employees..lastName | false | false | false | false | string | true | false | false | true | |||
3 | firstName | .employees..firstName | false | false | false | false | string | true | false | false | true | |||
4 | extension | .employees..extension | false | false | false | false | string | true | false | false | true | |||
5 | .employees..email | false | false | false | false | string | true | false | false | true | ||||
6 | officeCode | .officeCode | false | false | false | false | string | true | false | false | true | |||
7 | reportsTo | .manager.employeeNumber | false | false | false | false | number | false | false | true | false | |||
8 | jobTitle | .employees..jobTitle | false | false | false | false | string | true | false | false | true |
Point worth noting:
- We mapped the
reportsTo
to.manager.employeeNumber
manually.
Optimistic Locking
The repository service supports optimistic locking through a version control column. We can designate an integer column to be the version control column by setting its version
attribute to true
in the column bindings.
For example, we can hypothetically add a version
column to the orders
table as
and enable optimistic locking on this table by designating this new column as the version control column. To do this, we set the version
attribute for this new column to true
in the column bindings, as
{
"position": 1,
"column": "version",
"field": ".version",
"key": false,
"autoGenerate": false,
"inputField": null,
"insertValue": null,
"updateValue": null,
"version": true,
"softDelete": false,
"dataType": "number",
"notNull": false,
"keyEligible": false,
"versionEligible": false,
"softDeleteEligible": true
},
This column needs to be mapped to a version field of the object. It will be automatically set to 1
at creation, and incremented by each update operation. The service engine will make sure the updated version matches the version in database.
Soft Delete
The repository service supports soft delete through a soft delete column. We can designate a char
column to be the soft delete column by setting its softDelete
attribute to true
in the column bindings.
For example, we can hypothetically add a deleted
column to the orders
table as
and enable optimistic locking on this table by designating this new column as the soft delete column. To do this, we set the softDelete
attribute for this new column to true
in the column bindings , as
{
"position": 1,
"column": "deleted",
"field": null,
"key": false,
"autoGenerate": false,
"inputField": null,
"insertValue": null,
"updateValue": null,
"version": false,
"softDelete": true,
"dataType": "string",
"notNull": false,
"keyEligible": false,
"versionEligible": false,
"softDeleteEligible": true
},
This column is not mapped to any data field. However, it will be automatically populated with 'N' by the create operation and updated to 'Y' by the delete operation.
Conclusions
The repository service may be used to create repository for simple object mapped to a single table to complex object mapped to many tables, with or without read-write asymmetry. The key of the repository service development is in writing the SQL query for read. Then the write
component can be generated from the read
component and edited by the developer.