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

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) is fully supported.

The model changes that are not supported are listed below. Note that in most cases, the deployment of a new BDM will not raise an error, nor does it log anything, and completes "successfully".

  • Deleting an Object (table) → the object will not be deleted

  • Deleting a column → the column will not be deleted

  • Deleting an Index → the index will not be deleted

  • Deleting an unique Constraint → the constraint will not be deleted

  • Changing an attribute from a single type to multiple, for simple types → a new table is created, the column is not deleted in the corresponding business object table, and the data is not moved to the new table, and so is lost in the update.

  • Changing an attribute from a multiple type to single, for simple types → the table is not deleted, a new column is created in the corresponding business object table. The data is not moved,

  • Adding a new object with the same name as a deleted object, but different attributes → Both old and new objects are kept in the database, and share a table. Old lines are kept in the database with null value for new columns. New lines have null values in the old columns.

  • Changing an attribute from a multiple type to single, for object types (aggregation) → The intermediate table remains in the database, but is not used.

In all the above cases, the deployed BDM model does not match the database schema, but it will still probably work as expected.

The cases below are more problematic:

  • Changing the type of an attribute (primitive types) → the type will not be changed. Trying to instantiate a new object will probably lead to a runtime error : the database will try to convert the types, so it might work for some changes (Float → String), but won’t for others (String → Float).

  • Changing the max size of a String attribute → the size won’t change in the database. If the change was to reduce the size, no error will be raised. If the change was to increase the size, when trying to insert a longer String in an object, a runtime error will be raised.

  • Make a column "mandatory" → the column will not be made mandatory. The attribute in the java model is mandatory now, and it will cause an error when instantiating the process with empty value.

  • Delete a "mandatory" column → the column will not be deleted. Runtime errors will be raised during process execution.

  • Create a new "mandatory" column → the column cannot be created. The BDM deployment will fail.

  • Add a new unique constraint → the constraint is not added, even if there is no conflicting data. Will probably lead to errors at runtime.

  • Delete an unique constraint → the constraint is not deleted. Will probably lead to errors at runtime.

  • Changing an attribute from a single type to multiple, for object types (aggregation) → A new table is created. However, foreign key constraints are not deleted, and existing values are not transfered to the new table. Because of the foreign key, no object in the new collection can be deleted.

  • Changing an attribute from a single type to multiple, for object types (composition) → Two new columns are created. The update will fail if the table of the parent object had any data in it.

  • Changing an attribute from a multiple type to single, for object types (composition) → The existing referencing columns are not deleted. Because of the existing NOT NULL constraint on them, no new data can be inserted in the table.

If you need to update the BDM, and your update includes one of the cases listed above, 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