@rikishi/watermelondb
Version:
Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast
397 lines (311 loc) • 23.1 kB
HTML
<html lang="en" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Migrations - WatermelonDB documentation</title>
<!-- Custom HTML head -->
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body>
<!-- Provide site root to javascript -->
<script type="text/javascript">
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded "><a href="../ch01-00-get-excited.html"><strong aria-hidden="true">1.</strong> Get excited</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../index.html"><strong aria-hidden="true">1.1.</strong> Check out the README</a></li><li class="chapter-item expanded "><a href="../Demo.html"><strong aria-hidden="true">1.2.</strong> See the demo</a></li></ol></li><li class="chapter-item expanded "><a href="../ch02-00-learn-to-use.html"><strong aria-hidden="true">2.</strong> Learn to use Watermelon</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../Installation.html"><strong aria-hidden="true">2.1.</strong> Installation</a></li><li class="chapter-item expanded "><a href="../Setup.html"><strong aria-hidden="true">2.2.</strong> Setup</a></li><li class="chapter-item expanded "><a href="../Schema.html"><strong aria-hidden="true">2.3.</strong> Schema</a></li><li class="chapter-item expanded "><a href="../Model.html"><strong aria-hidden="true">2.4.</strong> Defining Models</a></li><li class="chapter-item expanded "><a href="../CRUD.html"><strong aria-hidden="true">2.5.</strong> Create, Read, Update, Delete</a></li><li class="chapter-item expanded "><a href="../Components.html"><strong aria-hidden="true">2.6.</strong> Connecting to React Components</a></li><li class="chapter-item expanded "><a href="../Query.html"><strong aria-hidden="true">2.7.</strong> Querying</a></li><li class="chapter-item expanded "><a href="../Relation.html"><strong aria-hidden="true">2.8.</strong> Relations</a></li><li class="chapter-item expanded "><a href="../Writers.html"><strong aria-hidden="true">2.9.</strong> Writers, Readers, batching</a></li></ol></li><li class="chapter-item expanded "><a href="../ch03-00-advanced.html"><strong aria-hidden="true">3.</strong> Advanced guides</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../Advanced/Migrations.html" class="active"><strong aria-hidden="true">3.1.</strong> Migrations</a></li><li class="chapter-item expanded "><a href="../Advanced/Sync.html"><strong aria-hidden="true">3.2.</strong> Sync</a></li><li class="chapter-item expanded "><a href="../Advanced/CreateUpdateTracking.html"><strong aria-hidden="true">3.3.</strong> Automatic create/update tracking</a></li><li class="chapter-item expanded "><a href="../Advanced/AdvancedFields.html"><strong aria-hidden="true">3.4.</strong> Advanced fields</a></li><li class="chapter-item expanded "><a href="../Advanced/Flow.html"><strong aria-hidden="true">3.5.</strong> Flow</a></li><li class="chapter-item expanded "><a href="../Advanced/LocalStorage.html"><strong aria-hidden="true">3.6.</strong> LocalStorage</a></li><li class="chapter-item expanded "><a href="../Advanced/ProTips.html"><strong aria-hidden="true">3.7.</strong> Pro tips</a></li><li class="chapter-item expanded "><a href="../Advanced/Performance.html"><strong aria-hidden="true">3.8.</strong> Performance tips</a></li><li class="chapter-item expanded "><a href="../Advanced/SharingDatabaseAcrossTargets.html"><strong aria-hidden="true">3.9.</strong> iOS - Sharing database across targets</a></li></ol></li><li class="chapter-item expanded "><a href="../ch04-00-deeper.html"><strong aria-hidden="true">4.</strong> Dig deeper into WatermelonDB</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../Implementation/Architecture.html"><strong aria-hidden="true">4.1.</strong> Architecture</a></li><li class="chapter-item expanded "><a href="../Implementation/Adapters.html"><strong aria-hidden="true">4.2.</strong> Adapters</a></li><li class="chapter-item expanded "><a href="../Implementation/SyncImpl.html"><strong aria-hidden="true">4.3.</strong> Sync implementation</a></li></ol></li><li class="chapter-item expanded "><a href="../ch04-00-deeper.html"><strong aria-hidden="true">5.</strong> Other</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../Roadmap.html"><strong aria-hidden="true">5.1.</strong> Roadmap</a></li><li class="chapter-item expanded "><a href="../CONTRIBUTING.html"><strong aria-hidden="true">5.2.</strong> Contributing</a></li><li class="chapter-item expanded "><a href="../CHANGELOG.html"><strong aria-hidden="true">5.3.</strong> Changelog</a></li></ol></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light (default)</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">WatermelonDB documentation</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="migrations"><a class="header" href="#migrations">Migrations</a></h1>
<p><strong>Schema migrations</strong> is the mechanism by which you can add new tables and columns to the database in a backward-compatible way.</p>
<p>Without migrations, if a user of your app upgrades from one version to another, their local database will be cleared at launch, and they will lose all their data.</p>
<p>⚠️ Always use migrations!</p>
<h2 id="migrations-setup"><a class="header" href="#migrations-setup">Migrations setup</a></h2>
<ol>
<li>
<p>Add a new file for migrations:</p>
<pre><code class="language-js">// app/model/migrations.js
import { schemaMigrations } from '@rikishi/watermelondb/Schema/migrations'
export default schemaMigrations({
migrations: [
// We'll add migration definitions here later
],
})
</code></pre>
</li>
<li>
<p>Hook up migrations to the Database adapter setup:</p>
<pre><code class="language-js">// index.js
import migrations from 'model/migrations'
const adapter = new SQLiteAdapter({
schema: mySchema,
migrations,
})
</code></pre>
</li>
</ol>
<h2 id="migrations-workflow"><a class="header" href="#migrations-workflow">Migrations workflow</a></h2>
<p>When you make schema changes when you use migrations, be sure to do this in this specific order, to minimize the likelihood of making an error.</p>
<h3 id="step-1-add-a-new-migration"><a class="header" href="#step-1-add-a-new-migration">Step 1: Add a new migration</a></h3>
<p>First, define the migration - that is, define the <strong>change</strong> that occurs between two versions of schema (such as adding a new table, or a new table column).</p>
<p>Don't change the schema file yet!</p>
<pre><code class="language-js">// app/model/migrations.js
import { schemaMigrations, createTable } from '@rikishi/watermelondb/Schema/migrations'
export default schemaMigrations({
migrations: [
{
// ⚠️ Set this to a number one larger than the current schema version
toVersion: 2,
steps: [
// See "Migrations API" for more details
createTable({
name: 'comments',
columns: [
{ name: 'post_id', type: 'string', isIndexed: true },
{ name: 'body', type: 'string' },
],
}),
],
},
],
})
</code></pre>
<p>Refresh your simulator/browser. You should see this error:</p>
<blockquote>
<p>Migrations can't be newer than schema. Schema is version 1 and migrations cover range from 1 to 2</p>
</blockquote>
<p>If so, good, move to the next step!</p>
<p>But you might also see an error like "Missing table name in schema", which means you made an error in defining migrations. See <a href="#migrations-api">"Migrations API" below</a> for details.</p>
<h3 id="step-2-make-matching-changes-in-schema"><a class="header" href="#step-2-make-matching-changes-in-schema">Step 2: Make matching changes in schema</a></h3>
<p>Now it's time to make the actual changes to the schema file — add the same tables or columns as in your migration definition</p>
<p>⚠️ Please double and triple check that your changes to schema match exactly the change you defined in the migration. Otherwise you risk that the app will work when the user migrates, but will fail if it's a fresh install — or vice versa.</p>
<p>⚠️ Don't change the schema version yet</p>
<pre><code class="language-js">// model/schema.js
export default appSchema({
version: 1,
tables: [
// This is our new table!
tableSchema({
name: 'comments',
columns: [
{ name: 'post_id', type: 'string', isIndexed: true },
{ name: 'body', type: 'string' },
],
}),
// ...
]
})
</code></pre>
<p>Refresh the simulator. You should again see the same "Migrations can't be newer than schema" error. If you see a different error, you made a syntax error.</p>
<h3 id="step-3-bump-schema-version"><a class="header" href="#step-3-bump-schema-version">Step 3: Bump schema version</a></h3>
<p>Now that we made matching changes in the schema (source of truth about tables and columns) and migrations (the change in tables and columns), it's time to commit the change by bumping the version:</p>
<pre><code class="language-js">// model/schema.js
export default appSchema({
version: 2,
tables: [
// ...
]
})
</code></pre>
<p>If you refresh again, your app should show up without issues — but now you can use the new tables/columns</p>
<h3 id="step-4-test-your-migrations"><a class="header" href="#step-4-test-your-migrations">Step 4: Test your migrations</a></h3>
<p>Before shipping a new version of the app, please check that your database changes are all compatible:</p>
<ol>
<li>Migrations test: Install the previous version of your app, then update to the version you're about to ship, and make sure it still works</li>
<li>Fresh schema install test: Remove the app, and then install the <em>new</em> version of the app, and make sure it works</li>
</ol>
<h3 id="why-is-this-order-important"><a class="header" href="#why-is-this-order-important">Why is this order important</a></h3>
<p>It's simply because React Native simulator (and often React web projects) are configured to automatically refresh when you save a file. You don't want to database to accidentally migrate (upgrade) with changes that have a mistake, or changes you haven't yet completed making. By making migrations first, and bumping version last, you can double check you haven't made a mistake.</p>
<h2 id="migrations-api"><a class="header" href="#migrations-api">Migrations API</a></h2>
<p>Each migration must migrate to a version one above the previous migration, and have multiple <em>steps</em> (such as adding a new table, or new columns). Larger example:</p>
<pre><code class="language-js">schemaMigrations({
migrations: [
{
toVersion: 3,
steps: [
createTable({
name: 'comments',
columns: [
{ name: 'post_id', type: 'string', isIndexed: true },
{ name: 'body', type: 'string' },
],
}),
addColumns({
table: 'posts',
columns: [
{ name: 'subtitle', type: 'string', isOptional: true },
{ name: 'is_pinned', type: 'boolean' },
],
}),
],
},
{
toVersion: 2,
steps: [
// ...
],
},
],
})
</code></pre>
<h3 id="migration-steps"><a class="header" href="#migration-steps">Migration steps:</a></h3>
<ul>
<li><code>createTable({ name: 'table_name', columns: [ ... ] })</code> - same API as <code>tableSchema()</code></li>
<li><code>addColumns({ table: 'table_name', columns: [ ... ] })</code> - you can add one or multiple columns to an existing table. The columns table has the same format as in schema definitions</li>
<li>Other types of migrations (e.g. deleting or renaming tables and columns) are not yet implemented. See <a href="https://github.com/Nozbe/WatermelonDB/blob/master/src/Schema/migrations/index.js"><code>migrations/index.js</code></a>. Please contribute!</li>
</ul>
<h2 id="database-reseting-and-other-edge-cases"><a class="header" href="#database-reseting-and-other-edge-cases">Database reseting and other edge cases</a></h2>
<ol>
<li>When you're <strong>not</strong> using migrations, the database will reset (delete all its contents) whenever you change the schema version.</li>
<li>If the migration fails, the database will fail to initialize, and will roll back to previous version. This is unlikely, but could happen if you, for example, create a migration that tries to create the same table twice. The reason why the database will fail instead of reset is to avoid losing user data (also it's less confusing in development). You can notice the problem, fix the migration, and ship it again without data loss.</li>
<li>When database in the running app has <em>newer</em> database version than the schema version defined in code, the database will reset (clear its contents). This is useful in development</li>
<li>If there's no available migrations path (e.g. user has app with database version 4, but oldest migration is from version 10 to 11), the database will reset.</li>
</ol>
<h3 id="rolling-back-changes"><a class="header" href="#rolling-back-changes">Rolling back changes</a></h3>
<p>There's no automatic "rollback" feature in Watermelon. If you make a mistake in migrations during development, roll back in this order:</p>
<ol>
<li>Comment out any changes made to schema.js</li>
<li>Comment out any changes made to migrations.js</li>
<li>Decrement schema version number (bring back the original number)</li>
</ol>
<p>After refreshing app, the database should reset to previous state. Now you can correct your mistake and apply changes again (please do it in order described in "Migrations workflow").</p>
<h3 id="unsafe-sql-migrations"><a class="header" href="#unsafe-sql-migrations">Unsafe SQL migrations</a></h3>
<p>Similar to <a href="../Schema.html">Schema</a>, you can add <code>unsafeSql</code> parameter to every migration step to modify or replace SQL generated by WatermelonDB to perform the migration. There is also an <code>unsafeExecuteSql('some sql;')</code> step you can use to append extra SQL. Those are ignored with LokiJSAdapter and for the purposes of <a href="./Sync.html">migration syncs</a>.</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../ch03-00-advanced.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../Advanced/Sync.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../ch03-00-advanced.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../Advanced/Sync.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script type="text/javascript">
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="../clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="../book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS scripts -->
</body>
</html>