Business Data Model (BDM) deployment

Lean how the engine handles the BDM deployment and redeployment.

Deployment flow

To deploy a BDM, we need to pass a ZIP file containing the model definition as an XML file, like the one below:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<businessObjectModel xmlns="http://documentation.bonitasoft.com/bdm-xml-schema/1.0" modelVersion="1.0" productVersion="7.13.0-SNAPSHOT">
    <businessObjects>
        <businessObject qualifiedName="com.acme.operations.Invoice">
            <fields>
                <field type="STRING" length="255" name="customerId" nullable="false" collection="false"/>
                <field type="STRING" length="255" name="externalReference" nullable="true" collection="false"/>
                <field type="LONG" length="255" name="CID_Code" nullable="false" collection="false"/>
            </fields>
            <uniqueConstraints/>
            <queries>
                <query name="searchByCustomer" content="SELECT i &#10;FROM Invoice i &#10;WHERE i.customerId = :customerId&#10;ORDER BY i.persistenceId ASC" returnType="com.acme.operations.Invoice">
                    <queryParameters>
                        <queryParameter name="customerId" className="java.lang.String"/>
                    </queryParameters>
                </query>
            </queries>
            <indexes/>
        </businessObject>
    </businessObjects>
</businessObjectModel>

Installing a new BDM executes the following steps:

  • generate a Java model from the XML model

  • generate and execute the SQL instructions to create the database tables

  • provide a Java client JAR file to programmatically interact with this model from any client application

BDM classes Generation

From the XML model, Java classes are generated, that match the same name, as the example below (incomplete extract):

package com.acme.operations;

@javax.persistence.Entity(name = "Invoice")
@Table(name = "INVOICE")
@NamedQueries({
@NamedQuery(name = "Invoice.findByPersistenceId", query = "SELECT i\nFROM Invoice i\nWHERE i.persistenceId= :persistenceId\n"),
@NamedQuery(name = "Invoice.findByCustomerId", query = "SELECT i\nFROM Invoice i\nWHERE i.customerId= :customerId\nORDER BY i.persistenceId"),
@NamedQuery(name = "Invoice.findByExternalReference", query = "SELECT i\nFROM Invoice i\nWHERE i.externalReference= :externalReference\nORDER BY i.persistenceId"),
@NamedQuery(name = "Invoice.findByCID_Code", query = "SELECT i\nFROM Invoice i\nWHERE i.CID_Code= :CID_Code\nORDER BY i.persistenceId"),
@NamedQuery(name = "Invoice.find", query = "SELECT i\nFROM Invoice i\nORDER BY i.persistenceId"),
@NamedQuery(name = "Invoice.countForFindByCustomerId", query = "SELECT COUNT(i)\nFROM Invoice i\nWHERE i.customerId= :customerId\n"),
})
public class Invoice implements org.bonitasoft.engine.bdm.Entity
{

    @Id
    @GeneratedValue(generator = "default_bonita_seq_generator")
    @GenericGenerator(name = "default_bonita_seq_generator", strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator", parameters = {
        @Parameter(name = "sequence_name", value = "hibernate_sequence")
    })
    private Long persistenceId;
    @Version
    private Long persistenceVersion;
    @Column(name = "CUSTOMERID", nullable = false, length = 255)
    private String customerId;
    @Column(name = "EXTERNALREFERENCE", nullable = true, length = 255)
    private String externalReference;
    @Column(name = "CID_CODE", nullable = false)
    private Long CID_Code;

    public Invoice() { }

    public void setCustomerId(String customerId) {
        this.customerId = customerId;
    }

    public String getCustomerId() {
        return customerId;
    }

}

Also, DAO interfaces and implementations are generated, to give access to the query methods, that contain code like:

    public com.acme.operations.Invoice findByPersistenceId(Long persistenceId) {
        try {
            CommandAPI commandApi = org.bonitasoft.engine.api.TenantAPIAccessor.getCommandAPI(session);
            Map<String, Serializable> commandParameters = new HashMap<String, Serializable>();
            commandParameters.put("queryName", "Invoice.findByPersistenceId");
            commandParameters.put("returnsList", false);
            commandParameters.put("returnType", "com.acme.operations.Invoice");
            Map<String, Serializable> queryParameters = new HashMap<String, Serializable>();
            queryParameters.put("persistenceId", persistenceId);
            commandParameters.put("queryParameters", ((Serializable) queryParameters));
            return proxyfier.proxify(deserializer.deserialize(((byte[]) commandApi.execute("executeBDMQuery", commandParameters)), com.acme.operations.Invoice.class));
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

DAO implementation code uses Engine Commands to call from client-side queries that run server-side.

Once generated, those Java classes are compiled and packaged:

  • in a server JAR that is stored in database and loaded in tenant (and process) classloaders

  • in a client JAR that can be retrieved by calling the getClientBDMZip() method, to be able to call from a client application. Below is a sample code on how it can be done.

...
byte[] clientBDMZip = getTenantAdministrationAPI().getClientBDMZip();
// clientBDMZip will typically contain : "README.md", "example-pom.xml", "bdm-dao.jar", "bdm-model.jar", "bom.zip"
final Map<String, byte[]> resources = new HashMap<>();
try (final ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(clientBDMZip))) {
    ZipEntry entry = zis.getNextEntry();
    while (entry != null) {
        try(final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            int len;
            final byte[] buffer = new byte[1024];
            while ((len = zis.read(buffer)) > 0) {
                baos.write(buffer, 0, len);
            }
            resources.put(entry.getName(), baos.toByteArray());
        }
        entry = zis.getNextEntry();
    }
}

byte[] bdmModelJar = resources.get("bdm-model.jar");
byte[] bdmDaoJar = resources.get("bdm-dao.jar");
...

Database tables creation and update

From this Java model, the SQL instructions to create / update the BDM database tables are generated and executed.

The class responsible for updating the database from the Java model is SchemaManagerUpdate, that basically delegates the implementation to Hibernate hbm2ddl.

An alternative implementation, SchemaManagerReadOnly, can be configured to NOT let Bonita update directly the database from the generated Java classes, but rather let a database administrator (DBA) handle this operation manually.
However, the SQL instructions run by the DBA must precisely match the Java classes for the BDM to be operational.

APIs

The TenantAdministrationAPI exposes several methods:

  • installBusinessDataModel() that installs a new BDM. Fails if one already exists. Call uninstallBusinessDataModel() first if a BDM already exists.

  • uninstallBusinessDataModel() that removes the JAR file from the Bonita database and unloads it from the tenant classloader (and from all the process classloaders of the tenant). Does not update or remove anything from the database.

  • cleanAndUninstallBusinessDataModel(): same as above plus drops all the BDM tables resulting in a complete data loss. Not for production. Use with caution.

  • other useful methods

Deploying a new version of the BDM

Deploying a new version of the BDM on top of an existing BDM needs to uninstall the previous version first.
The java classes of the model are generated again from scratch, so any refactoring is supported.
On the other side, the database refactoring is only partially supported. See limitations below.

Constraints to (re-)deploy a BDM

A BDM is defined at tenant level.

To be able to deploy or redeploy a BDM, the BPM services of this tenant must be paused first.

Indeed, as deploying the BDM changes the tenant classloader and all process classloaders on the same tenant, no process can safely run during this operation. Pausing the services means that the work service does not execute works anymore on this tenant, the Job service does not triggers any job anymore on this tenant, etc. Only the vital services stay alive, to be able to operate the BDM and the rest of the platform. Other tenants are not affected.

After (re-)deploying the BDM, the BPM services must be resumed to be able to use

Deploying a BDM via the Bonita Portal

Using the Bonita Portal to deploy a BDM follows this flow:

  • you must be logged in as the tenant administrator

  • go to the BPM services page, and pause the services

  • go to the Business Data Model page, select the BDM zip file to deploy

  • if a BDM already exists on the current tenant, the portal implicitly calls uninstallBusinessDataModel() on the Engine API

  • then the portal calls installBusinessDataModel() on the Engine API, which generates the Java model, loads it in the tenant classloader (and in the process classloaders of all processes on this tenant), and creates / updates the database accordingly.

  • finally, go to the BPM services page, and resume the services

Then the new BDM is directly available, even if in most cases, new (versions of) processes must be (re-)deployed to take advantage of the BDM.

Limitations

Regarding the deployment of a new version of the BDM, Hibernate hbm2ddl can handle certain changes, but not all model changes are supported.

Adding a new Object (table) or adding a new attribute on an existing Object (new column on an existing database table) are fully supported.

The model changes that are not supported are:

  • Deleting an Object (table)

  • Deleting a column

  • Deleting an Index

  • Deleting an unique Constraint

  • Changing the max size of a String attribute.

  • Changing an attribute from a single type to multiple, for simple types

  • Changing an attribute from a multiple type to single, for simple types

In the above cases, there will be no error when deploying the BDM, but no change will be made to the database. It will probably lead to failure when instantiating new objects.

Other not supported model changes are:

  • Adding a new object with the same name as a deleted object, but different attributes

  • Make a column "mandatory", delete a "mandatory" column, or create a new "mandatory" column

  • Add or delete a new unique constraint

  • Changing an attribute from a single type to multiple, for object types

  • Changing an attribute from a multiple type to single, for object types

These cases will raise an error during the automatic BDM update.

If you need to update the BDM, and your update includes one of the cases listed above (both those that generate an error at installation, and those that don’t), you will need to do the update manually:

  1. Stop your Tenant services

  2. Stop your Bonita server

  3. Update manually the BDM schema in your BDM database to make it correspond to your new BDM. The easiest way to do it is to first install your new BDM on a clean database. Then compare the new schema with the old one, and manually create a sql script to update your BDM database to match the new one. Apply this sql script to your BDM database

  4. Restart your Bonita server

  5. Install your new BDM the usual way

  6. Restart your tenant services