Automatic Versioning in Mobile Apps
TL;DR: This article presents an automatic versioning and migration framework for mobile app databases using schema hashes and seamless upgrades.
Join the DZone community and get the full member experience.
Join For FreePicture this: Your mobile app is evolving quickly, with new features requiring changes to the local database. Your users expect seamless upgrades, and your stakeholders want new functionality delivered fast. The challenge becomes even trickier when you realize you need to update the app's database schema without disrupting existing data or forcing users into error-prone migrations.
In this article, I’ll share a practical, automatic versioning approach I developed that simplifies database versioning and migration for mobile apps. This system lets developers focus on data models instead of manually scripting version changes, while making sure users’ data stays intact across updates.
The Challenge
Traditionally, handling local database changes in mobile apps means:
- Keeping track of schema versions
- Writing manual scripts to modify tables
- Handling special cases where users jump over versions (e.g., upgrade from v1 straight to v4)
- Debugging mismatches between app versions and database versions
It’s error-prone, time-consuming, and a pain to test. There had to be a better way, so I built one.
Automatic Versioning Approach
1. Model Classes as the Source of Truth
Instead of manually tracking versions, the data models themselves drive everything. Any changes developers make to the model classes automatically trigger schema updates. This way, developers only need to think about the data they need, not how to evolve the schema.
public class TB
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
// ...
}
The schema for the database comes directly from these models — no manual scripts required.
2. Automatic Script Generation
A helper class generates the SQL script based on the model classes. Converting model classes into SQL scripts is a well-known practice and is not included in this article. Instead, this article focuses on what happens after the script is generated — specifically, that script is hashed to create a unique version identifier for the schema.
public class DatabaseHelper
{
public string GenerateDatabaseScript()
{
// Logic to generate the database script based on model classes
}
public string GetDatabaseName(string databaseScriptHashcode)
{
// Logic to generate the database name based on the hash code of the database script
return $“{GetDatabaseNamePrefix()}_v{databaseScriptHashcode}.db”;
}
public string GetDatabaseNamePrefix()
{
// Logic to generate the optional database name’s prefix
}
// ...
}
Every schema change automatically results in a new hash, so versioning is tied directly to the schema itself, not some manually maintained version number.
3. Dynamic Database Initialization
At app launch, the system:
- Generates the current schema script.
- Computes the hash code for that script.
- Checks if a local database file with the matching hash code already exists.
- If no match is found, a new database is created using the current schema.
public class DatabaseInitializer
{
private readonly DatabaseHelper databaseHelper;
public void InitializeDatabase()
{
string databaseScript = databaseHelper.GenerateDatabaseScript();
string databaseName = databaseHelper.GetDatabaseName(GetHashCode(databaseScript));
// Check if the database file with the current schema’s hash code exists
if (!DoesMatchingDatabaseExist(databaseName))
{
// No matching database found, create a new database
CreateDatabase(databaseScript);
}
}
private bool DoesMatchingDatabaseExist()
{
// Logic to check if the database file with the specified name exists
}
private void CreateDatabase(string databbaseScript)
{
// Logic to create a new database based on the database script
}
public string GetHashCode(string databbaseScript)
{
// Logic to create hash code
}
// ...
}
This ensures every schema change results in a clean, correctly versioned database.
4. Generating a Fixed-Length Hash Code
One small challenge is that .NET’s built-in GetHashCode()
can generate:
- Different hashes on different platforms.
- Hashes of varying lengths, which are awkward for file names.
To make things predictable, one may use a simple function like the one below that produces a 6-digit numeric hash. This keeps the file name consistent and easy to read.
static int GetHashCode(string s)
{
int hashValue = 0;
foreach (char c in s)
{
hashValue = (hashValue * 31 + c) % 1000000;
}
return hashValue;
}
This gives you:
- A short, human-friendly hash.
- Predictable behavior across devices.
- Enough uniqueness to handle schema changes.
Data Migration Made Simple
1. Unified Initialization and Migration
This system doesn’t just create new databases — it also handles migrating data from the previous schema version if needed.
- When a new schema is detected (i.e., hash has changed), a new database is created.
- The system looks for the previous database (by its hash).
- If found, data is migrated automatically.
public class DatabaseInitializer
{
private readonly DatabaseHelper databaseHelper;
private readonly DatabaseMigrationHelper migrationHelper;
public void InitializeDatabase()
{
string currentSchemaScript = databaseHelper.GenerateDatabaseScript();
string currentSchemaHashCode = GetHashCode(currentSchemaScript);
// Check if the database file with the current schema's hash code exists
if (!DoesMatchingDatabaseExist(currentSchemaHashCode))
{
// No matching database found, create a new database
CreateDatabase(currentSchemaScript);
// Check if the database file with the previous schema's hash code exists
string previousSchemaHashCode = RetrievePreviousSchemaHashCode();
if (!string.IsNullOrEmpty(previousSchemaHashCode) && DoesMatchingDatabaseExist(previousSchemaHashCode))
{
// Matching database found
if (previousSchemaHashCode != currentSchemaHashCode)
{
// Previous schema is different, perform data migration
migrationHelper.PerformDataMigration(previousSchemaHashCode, currentSchemaHashCode);
}
// Store the hash code of the current schema for future reference
StoreCurrentSchemaHashCode(currentSchemaHashCode);
}
}
}
// ...
}
This migration logic only runs if the schema actually changed, saving unnecessary processing.
2. Data Migration Helper
The migration itself is handled by a dedicated class. It copies data table-by-table from the old database to the new one. If any errors occur, they can be logged, and appropriate logic can be applied to handle the migration process gracefully.
public class DatabaseMigrationHelper
{
public void PerformDataMigration(string previousDatabaseName, string currentDatabaseName)
{
try
{
CopyData(previousDatabaseName, currentDatabaseName);
}
catch (Exception ex)
{
// Handle any potential errors during data migration
}
}
// ...
}
Conclusion
This model-driven approach makes database versioning and migration practically invisible to developers. They focus on models, not migration scripts.
- Schema changes automatically trigger new database versions.
- Data is preserved when possible.
- No manual version tracking is needed.
For teams working on rapidly evolving mobile apps, this kind of system removes one of the biggest headaches: schema management. It’s also future-proof — the same approach could work for desktop apps, embedded apps, or even local-first web apps.
This solution has saved my team and me countless hours of manual schema tracking and migration work, and I believe it could help others in the same boat.
Opinions expressed by DZone contributors are their own.
Comments