8.4 Installation & Upgrade Codeunits, Data Transfer & Archive
Key Takeaways
- Installation codeunits (Subtype = Install) run during extension installation or synchronization, exposing OnInstallAppPerCompany and OnInstallAppPerDatabase triggers for initial configuration.
- Upgrade codeunits (Subtype = Upgrade) manage data transformations across versions via a strict, sequential six-stage trigger execution pipeline.
- The ModuleInfo record retrieved via NavApp.GetCurrentModuleInfo provides DataVersion and AppVersion properties to conditionally gate upgrade procedures based on previous database state.
- The DataTransfer object executes set-based, bulk SQL operations (CopyFields, CopyRows) directly on the database engine, bypassing slow row-by-row AL loops and preventing SaaS timeouts.
- Archive management methods (NavApp.RestoreArchiveData and NavApp.DeleteArchiveData) govern data recovery and cleanup when refactoring or deprecating obsolete tables.
8.4 Installation & Upgrade Codeunits, Data Transfer & Archive
When deploying, reinstalling, or updating extensions in Business Central environments, developers must ensure seamless database initialization and schema migration. AL provides specialized codeunit subtypes to handle these lifecycles: Install Codeunits (Subtype = Install) for first-time tenant setup, and Upgrade Codeunits (Subtype = Upgrade) for managing multi-version data transformations. Understanding the exact execution order, version evaluation APIs, and the modern DataTransfer set-based migration API is heavily tested on the MB-820 exam.
1. Installation Codeunits (Subtype = Install)
An Install codeunit executes automatically whenever an extension is installed or synchronized in a tenant. It contains system triggers that initialize configuration tables, generate number series, and register assisted setup wizards.
codeunit 50140 "App Installation Handler"
{
Subtype = Install;
trigger OnInstallAppPerCompany()
var
AppSetup: Record "Custom App Setup";
begin
// Executed for every individual company in the tenant
if not AppSetup.Get() then begin
AppSetup.Init();
AppSetup."Default Batch Name" := 'GENERAL';
AppSetup."Enable Telemetry" := true;
AppSetup.Insert(true);
end;
InitializeDefaultNumberSeries();
end;
trigger OnInstallAppPerDatabase()
begin
// Executed once across the entire tenant database
// Used for cross-company tables (DataPerCompany = false) or global database setup
end;
local procedure InitializeDefaultNumberSeries()
var
NoSeries: Record "No. Series";
NoSeriesLine: Record "No. Series Line";
begin
if not NoSeries.Get('CUST-PROMO') then begin
NoSeries.Init();
NoSeries.Code := 'CUST-PROMO';
NoSeries.Description := 'Customer Promotion Series';
NoSeries."Default Nos." := true;
NoSeries.Insert();
NoSeriesLine.Init();
NoSeriesLine."Series Code" := NoSeries.Code;
NoSeriesLine."Line No." := 10000;
NoSeriesLine."Starting No." := 'PRM0001';
NoSeriesLine."Ending No." := 'PRM9999';
NoSeriesLine.Insert();
end;
end;
}
Triggers and Idempotency Best Practices
OnInstallAppPerCompany(): Runs once for each individual company in the Business Central tenant database. It should populate company-specific setup tables and master records.OnInstallAppPerDatabase(): Runs once across the entire tenant database, regardless of how many companies exist. Ideal for tables configured withDataPerCompany = false.- Idempotency Rule: Install triggers can execute multiple times (e.g., if the extension is uninstalled and reinstalled, or when a new company is created). Code must be strictly idempotent—always verify if records exist (
if not Setup.Get() then ...) before inserting to prevent primary key collisions.
2. Upgrade Codeunits (Subtype = Upgrade)
When a newer version of an extension is deployed over an existing version, the platform invokes Upgrade codeunits. Upgrade codeunits run through a strict six-stage execution pipeline across database and company contexts.
The Six Upgrade Triggers in Execution Sequence
| Order | Trigger Name | Context | Purpose |
|---|---|---|---|
| 1 | OnCheckPreconditionsPerDatabase() | Database | Verify global database prerequisites before any upgrade step begins. |
| 2 | OnCheckPreconditionsPerCompany() | Company | Verify company-specific prerequisites; fail fast if required historical data is invalid. |
| 3 | OnUpgradePerDatabase() | Database | Execute data migrations on shared cross-company tables (DataPerCompany = false). |
| 4 | OnUpgradePerCompany() | Company | Execute primary data migrations, table transformations, and field mappings per company. |
| 5 | OnValidateUpgradePerDatabase() | Database | Validate data integrity and record counts across the global database post-upgrade. |
| 6 | OnValidateUpgradePerCompany() | Company | Validate that company-level records and migrated fields meet business rules. |
Failure Semantics & Rollback
If an unhandled error occurs during any trigger in the six-step sequence:
- The entire upgrade process terminates immediately.
- All database modifications made across the upgrade transaction are completely rolled back.
- The tenant remains on the existing extension version without data corruption.
Version-Checking with NavApp.GetCurrentModuleInfo
To ensure upgrade routines only run when updating from specific prior builds, developers inspect DataVersion (the version of the data currently stored in the database) against AppVersion (the version of the code package being installed).
codeunit 50145 "App Upgrade Handler"
{
Subtype = Upgrade;
trigger OnUpgradePerCompany()
var
AppInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(AppInfo);
// Gating upgrade logic: Run only if upgrading from Version 1.x to 2.x
if AppInfo.DataVersion.Major < 2 then
MigrateCustomerLegacyCodes();
end;
local procedure MigrateCustomerLegacyCodes()
begin
// Migration logic
end;
}
Exam Watchout — DataVersion vs. AppVersion:
AppInfo.DataVersion: Represents the version of the data currently persisted in the tenant database. For a fresh install on a clean tenant,DataVersion = Version.Create(0, 0, 0, 0).AppInfo.AppVersion: Represents the version of the incoming code extension package defined inapp.json.
3. High-Performance Bulk Migration: The DataTransfer Object
Historically, migrating data between tables during an upgrade required iterating through records with FindSet() and updating them row-by-row with Modify(). In cloud tenants with millions of rows, this generated massive transaction logs and timed out. AL introduced the DataTransfer object, which compiles directly into set-based SQL statements (INSERT INTO ... SELECT and UPDATE ... FROM), executing directly in the database engine.
codeunit 50150 "Fast Data Upgrade"
{
Subtype = Upgrade;
trigger OnUpgradePerCompany()
var
AppInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(AppInfo);
if AppInfo.DataVersion.Major < 2 then begin
CopyCustomerFieldsBulk();
MigrateLegacyTableRowsBulk();
end;
end;
local procedure CopyCustomerFieldsBulk()
var
Customer: Record Customer;
DataXfer: DataTransfer;
begin
// Set-based field copy within the same table
DataXfer.SetTables(Database::Customer, Database::Customer);
DataXfer.AddFieldValue(Customer.FieldNo("Legacy Loyalty Code"), Customer.FieldNo("Loyalty ID"));
DataXfer.AddSourceFilter(Customer.FieldNo("Legacy Loyalty Code"), '<>%1', '');
DataXfer.CopyFields(); // Emits a single direct SQL UPDATE statement
end;
local procedure MigrateLegacyTableRowsBulk()
var
LegacyStaging: Record "Legacy Staging Table";
NewArchive: Record "New Customer Archive";
DataXfer: DataTransfer;
begin
// Bulk copy rows from one table into a new table
DataXfer.SetTables(Database::"Legacy Staging Table", Database::"New Customer Archive");
DataXfer.AddFieldValue(LegacyStaging.FieldNo("Entry No."), NewArchive.FieldNo("Entry No."));
DataXfer.AddFieldValue(LegacyStaging.FieldNo("Customer Code"), NewArchive.FieldNo("Customer No."));
DataXfer.AddFieldValue(LegacyStaging.FieldNo("Amount"), NewArchive.FieldNo("Total Balance"));
DataXfer.AddConstantValue(Today(), NewArchive.FieldNo("Migrated Date"));
DataXfer.CopyRows(); // Emits a single direct SQL INSERT INTO ... SELECT statement
end;
}
DataTransfer Methods & Rules
SetTables(SourceTableId, TargetTableId): Specifies source and destination tables.AddFieldValue(SourceFieldNo, TargetFieldNo): Maps source columns to target columns.AddConstantValue(Value, TargetFieldNo): Populates target fields with a fixed constant value.AddJoin(SourceFieldNo, TargetFieldNo): Establishes join conditions between distinct source and target tables when updating fields.AddSourceFilter(SourceFieldNo, FilterString): Limits the SQL query to a filtered subset of rows.CopyFields(): Executes a set-based SQLUPDATE.CopyRows(): Executes a set-based SQLINSERT INTO ... SELECT.- Platform Restrictions:
DataTransfercan only be executed within Upgrade codeunits (Subtype = Upgrade). It bypasses table triggers and validation for maximum performance and cannot target system tables.
4. Data Archive Management & Table Deprecation
When a table or table extension is marked ObsoleteState = Removed or during breaking schema changes where fields are refactored into new extensions, the platform safeguards data using table archives.
Data Archive Lifecycle APIs
NavApp.RestoreArchiveData(Database::TableName): Restores archived data from a previous version into the target table during an upgrade.NavApp.DeleteArchiveData(Database::TableName): Permanently purges backed-up archive data once validation has succeeded, freeing database storage.
Comparison of Data Migration Approaches
| Migration Approach | Execution Mechanism | Performance on 1M+ Records | Trigger Execution | Permitted Context |
|---|---|---|---|---|
DataTransfer Object | Set-based SQL (UPDATE / INSERT SELECT) | Instantaneous (seconds) | No (Bypasses triggers) | Subtype = Upgrade only |
Record Looping (FindSet + Modify) | Row-by-row AL iteration | Extremely slow (minutes/hours; timeouts) | Optional (Modify(true)) | Anywhere in AL |
ModifyAll() | Parameterized SQL UPDATE | Fast | No | Anywhere in AL |
XMLport Export/Import | File-based streaming | Slow (I/O intensive) | Optional | Normal / Batch Jobs |
Which trigger in an Install codeunit (Subtype = Install) executes once for each individual company in a tenant when an extension is deployed or synchronized?
What is the correct sequential execution order of triggers during an extension upgrade in Business Central?
A developer needs to migrate 5 million records from an obsolete staging table to a new table structure during an extension upgrade. To achieve optimal performance and prevent transaction timeouts in SaaS, which technology should be used?
When inspecting ModuleInfo via NavApp.GetCurrentModuleInfo(AppInfo) during an upgrade, what does the AppInfo.DataVersion property represent?