Business Data Model technical handling

Learn how the engine handles the BDM code generation, database support, 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. Deprecated in 2021.2, as updateBusinessDataModel() covers the same use case, and should be used instead.

  • updateBusinessDataModel() that updates the BDM. It uninstalls the previous BDM if applicable, and installs the new one. The call is done in a single database transaction, which allows it to be rolled back if the update fails.

  • 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

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.

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 Super Administrator application

Internally, the runtime will call the method updateBusinessDataModel() to install the new BDM. This method implicitly performs the following steps in a single database transaction:

  • if a BDM already exists on the current tenant, it is uninstalled

  • the new BDM is installed / deployed

  • the Bonita Engine loads the new BDM in the tenant classloader (and in the process classloaders of all processes on this tenant), and creates / updates the database accordingly.