@rikishi/watermelondb
Version:
Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast
344 lines (261 loc) • 21.5 kB
HTML
<html lang="en" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Advanced fields - 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"><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" class="active"><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="advanced-fields"><a class="header" href="#advanced-fields">Advanced Fields</a></h1>
<h2 id="json"><a class="header" href="#json"><code>@json</code></a></h2>
<p>If you have a lot of metadata about a record (say, an object with many keys, or an array of values), you can use a <code>@json</code> field to contain that information in a single string column (serialized to JSON) instead of adding multiple columns or a relation to another table.</p>
<p>⚠️ This is an advanced feature that comes with downsides — make sure you really need it</p>
<p>First, add a string column to <a href="../Schema.html">the schema</a>:</p>
<pre><code class="language-js">tableSchema({
name: 'comments',
columns: [
{ name: 'reactions', type: 'string' }, // You can add isOptional: true, if appropriate
],
})
</code></pre>
<p>Then in the Model definition:</p>
<pre><code class="language-js">import { json } from '@rikishi/watermelondb/decorators'
class Comment extends Model {
// ...
@json('reactions', sanitizeReactions) reactions
}
</code></pre>
<p>Now you can set complex JSON values to a field:</p>
<pre><code class="language-js">comment.update(() => {
comment.reactions = ['up', 'down', 'down']
})
</code></pre>
<p>As the second argument, pass a <strong>sanitizer function</strong>. This is a function that receives whatever <code>JSON.parse()</code> returns for the serialized JSON, and returns whatever type you expect in your app. In other words, it turns raw, dirty, untrusted data (that might be missing, or malformed by a bug in previous version of the app) into trusted format.</p>
<p>The sanitizer might also receive <code>null</code> if the column is nullable, or <code>undefined</code> if the field doesn't contain valid JSON.</p>
<p>For example, if you need the field to be an array of strings, you can ensure it like so:</p>
<pre><code class="language-js">const sanitizeReactions = rawReactions => {
return Array.isArray(rawReactions) ? rawReactions.map(String) : []
}
</code></pre>
<p>If you don't want to sanitize JSON, pass an identity function:</p>
<pre><code class="language-js">const sanitizeReactions = json => json
</code></pre>
<p>The sanitizer function takes an optional second argument, which is a reference to the model. This is useful is your sanitization logic depends on the other fields in the model.</p>
<p><strong>Warning about JSON fields</strong>:</p>
<p>JSON fields go against relational, lazy nature of Watermelon, because <strong>you can't query or count by the contents of JSON fields</strong>. If you need or might need in the future to query records by some piece of data, don't use JSON.</p>
<p>Only use JSON fields when you need the flexibility of complex freeform data, or the speed of having metadata without querying another table, and you are sure that you won't need to query by those metadata.</p>
<h2 id="nochange"><a class="header" href="#nochange"><code>@nochange</code></a></h2>
<p>For extra protection, you can mark fields as <code>@nochange</code> to ensure they can't be modified. Always put <code>@nochange</code> before <code>@field</code> / <code>@date</code> / <code>@text</code></p>
<pre><code class="language-js">import { field, nochange } from '@rikishi/watermelondb/decorators'
class User extends Model {
// ...
@nochange @field('is_owner') isOwner
}
</code></pre>
<p><code>user.isOwner</code> can only be set in the <code>collection.create()</code> block, but will throw an error if you try to set a new value in <code>user.update()</code> block.</p>
<h3 id="readonly"><a class="header" href="#readonly"><code>@readonly</code></a></h3>
<p>Similar to <code>@nochange</code>, you can use the <code>@readonly</code> decorator to ensure a field cannot be set at all. Use this for <a href="./CreateUpdateTracking.html">create/update tracking</a>, but it might also be useful if you use Watermelon with a <a href="../Advanced/Sync.html">Sync engine</a> and a field can only be set by the server.</p>
<h2 id="custom-observable-fields"><a class="header" href="#custom-observable-fields">Custom observable fields</a></h2>
<p>You're in advanced <a href="https://github.com/ReactiveX/rxjs">RxJS</a> territory now! You have been warned.</p>
<p>Say, you have a Post model that has many Comments. And a Post is considered to be "popular" if it has more than 10 comments.</p>
<p>You can add a "popular" badge to a Post component in two ways.</p>
<p>One is to simply observe how many comments there are <a href="../Components.html">in the component</a>:</p>
<pre><code class="language-js">const enhance = withObservables(['post'], ({ post }) => ({
post: post.observe(),
commentCount: post.comments.observeCount()
}))
</code></pre>
<p>And in the <code>render</code> method, if <code>props.commentCount > 10</code>, show the badge.</p>
<p>Another way is to define an observable property on the Model layer, like so:</p>
<pre><code class="language-js">import { distinctUntilChanged, map as map$ } from 'rxjs/operators'
import { lazy } from '@rikishi/watermelondb/decorators'
class Post extends Model {
@lazy isPopular = this.comments.observeCount().pipe(
map$(comments => comments > 10),
distinctUntilChanged()
)
}
</code></pre>
<p>And then you can directly connect this to the component:</p>
<pre><code class="language-js">const enhance = withObservables(['post'], ({ post }) => ({
isPopular: post.isPopular,
}))
</code></pre>
<p><code>props.isPopular</code> will reflect whether or not the Post is popular. Note that this is fully observable, i.e. if the number of comments rises above/falls below the popularity threshold, the component will re-render. Let's break it down:</p>
<ul>
<li><code>this.comments.observeCount()</code> - take the Observable number of comments</li>
<li><code>map$(comments => comments > 10)</code> - transform this into an Observable of boolean (popular or not)</li>
<li><code>distinctUntilChanged()</code> - this is so that if the comment count changes, but the popularity doesn't (it's still below/above 10), components won't be unnecessarily re-rendered</li>
<li><code>@lazy</code> - also for performance (we only define this Observable once, so we can re-use it for free)</li>
</ul>
<p>Let's make this example more complicated. Say the post is <strong>always</strong> popular if it's marked as starred. So if <code>post.isStarred</code>, then we don't have to do unnecessary work of fetching comment count:</p>
<pre><code class="language-js">import { of as of$ } from 'rxjs/observable/of'
import { distinctUntilChanged, map as map$ } from 'rxjs/operators'
import { lazy } from '@rikishi/watermelondb/decorators'
class Post extends Model {
@lazy isPopular = this.observe().pipe(
distinctUntilKeyChanged('isStarred'),
switchMap(post =>
post.isStarred ?
of$(true) :
this.comments.observeCount().pipe(map$(comments => comments > 10))
),
distinctUntilChanged(),
)
}
</code></pre>
<ul>
<li><code>this.observe()</code> - if the Post changes, it might change its popularity status, so we observe it</li>
<li><code>this.comments.observeCount().pipe(map$(comments => comments > 10))</code> - this part is the same, but we only observe it if the post is starred</li>
<li><code>switchMap(post => post.isStarred ? of$(true) : ...)</code> - if the post is starred, we just return an Observable that emits <code>true</code> and never changes.</li>
<li><code>distinctUntilKeyChanged('isStarred')</code> - for performance, so that we don't re-subscribe to comment count Observable if the post changes (only if the <code>isStarred</code> field changes)</li>
<li><code>distinctUntilChanged()</code> - again, don't emit new values, if popularity doesn't change</li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../Advanced/CreateUpdateTracking.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/Flow.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="../Advanced/CreateUpdateTracking.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/Flow.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>