Daily-It

개발, AI, 인프라, 자동화와 일상 IT 제품 후기를 직접 써보며 정리하는 기술 블로그입니다.

React Native 中的 WatermelonDB 是什么?与 SQLite 相比,如何使用离线优先本地数据库

摘要

WatermelonDB 用于快速处理 React Native 应用程序中的大量本地数据。 是一个 离线优先的响应式数据库框架。内部存储主要使用 SQLite,但开发人员不再直接编写 SQL,而是通过 Schema、Model、Query、Writer 和 Sync API 构建应用程序数据流。对于简单的缓存或存储一些设置来说,这可能有点大材小用,但对于需要数千到数万条记录、同步、关系数据和自动屏幕更新的应用程序来说,它比直接使用 SQLite 更有效率。

如果你想先整理一下 SQLite 概念, Serverless 基于文件的 DB 的优点、局限性、WAL 和锁定错误解决方案是 SQLite 实用指南 ↗已经单独详细总结了。

验证标准:在撰写本文时,npm 的最新版本是 @nozbe/watermelondb 确认为0.28.0,但官方文档页面显示为0.27.1。在安装之前,检查项目的React Native/Expo版本和WatermelonDB版本/问题会更安全。

目录

背景

创建 React Native 应用程序时,起初似乎只需调用服务器 API 并绘制屏幕就足够了。但当下一个请求到来时,情况就发生了变化。

  • 即使网络断开也必须可以创建/编辑。
  • 该应用程序需要在启动后立即快速显示数千个任务、消息、订单和客户数据。
  • 当本地数据发生变化时,相关屏幕应自动更新。
  • 服务器和本地数据必须定期同步。
  • 需要的是关系数据,而不是简单的键值存储。

此时经常想到的选项是 SQLite。 SQLite接近移动本地DB的事实标准,具有良好的性能和稳定性。然而,在React Native中直接使用SQLite时,SQL编写、结果映射、屏幕更新、同步冲突处理、迁移等都必须直接在应用程序代码中设计。

WatermelonDB 是一个试图解决这个问题的工具。而不是取代 SQLite, 一个框架,在 SQLite 之上构建适合 React/React Native 应用程序的数据层。附近

什么是西瓜数据库?

WatermelonDB 是一个开源的数据库管理工具,它可以帮助您更好地管理数据。官方介绍的要点如下。

  • 它旨在快速处理 React Native 和 React 应用程序中的大量本地数据。
  • 不是一次性将所有数据加载到 JavaScript 内存中,而是在需要时延迟读取。
  • 查询在本机数据库(例如 SQLite)中执行。
  • 通过提供基于RxJS的可观察结构,可以在数据变化时自动更新UI。
  • 您可以通过连接自己的后端来实现离线优先级同步。

换句话说,WatermelonDB 不仅仅是一个 SQLite 包装器。以下各层一起提供。

成分 角色
Schema 表和列定义
Model 定义您的应用程序处理的数据对象
Collection 记录特定表的访问单元
Query API 基于条件的查找
Writer 编写安全捆绑创建/修改/删除操作的事务概念
Observable 数据变化时自动反映UI
Sync API 服务器和本地数据库之间的更改同步

为什么 WatermelonDB 很快

WatermelonDB 的性能点并不仅仅以“它很快,因为它是 SQLite”结束。更重要的部分是 不会将不必要的数据拉入 JavaScript 区域的结构全部。

一般状态管理+持久化的方式很容易在app启动时加载大量数据到JS内存中。数百条数据时没有问题,但当数量增加到数千或数万时,应用程序启动速度和内存占用会明显变差。

WatermelonDB 通过以下方式减少了这个问题:

1.启动应用程序时,不要将整个DB加载到JS内存中。 2. 仅在必要的屏幕上执行必要的查询。 3.条件搜索是通过DB查询来处理的,而不是JS数组过滤。 4. 只有连接到已更改数据的组件才会更新为可观察的。

如果您在 React Native 应用程序中遇到“随着本地数据量的增加,应用程序变得越来越重”的问题,那么值得考虑 WatermelonDB。

React Native 基本用法

下面的示例是一个用于存储博客文章和评论的简单结构。在实际项目中,您可以调整文件夹结构并相应地导入别名。

1. 安装

npm install @nozbe/watermelondb
npm install -D @babel/plugin-proposal-decorators

或者,如果您使用 Yarn,请按如下方式安装。

yarn add @nozbe/watermelondb
yarn add --dev @babel/plugin-proposal-decorators

WatermelonDB使用装饰器语法,因此需要Babel配置。

{
  "presets": ["module:metro-react-native-babel-preset"],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }]
  ]
}

在 iOS 上,可能需要设置 CocoaPods。官方文档是 simdjson 添加广告连播 pod install引导您完成

# ios/Podfile
pod 'simdjson', path: '../node_modules/@nozbe/simdjson', modular_headers: true
cd ios
pod install

我经常被困在这里。尤其 use_frameworks!、Expo 和 React Native 新架构组合需要根据项目状态进行额外确认。

2. 模式定义

在WatermelonDB中,首先定义了DB Schema。表名和列名通常是 snake_case使用 。

// src/db/schema.js
import { appSchema, tableSchema } from '@nozbe/watermelondb'

export const mySchema = appSchema({
  version: 1,
  tables: [
    tableSchema({
      name: 'posts',
      columns: [
        { name: 'title', type: 'string' },
        { name: 'body', type: 'string' },
        { name: 'is_pinned', type: 'boolean' },
      ],
    }),
    tableSchema({
      name: 'comments',
      columns: [
        { name: 'body', type: 'string' },
        { name: 'post_id', type: 'string', isIndexed: true },
      ],
    }),
  ],
})

post_id 这类经常用于关系查询的列,建议加上 isIndexed: true。就像在 SQLite 中索引很重要一样,在 WatermelonDB 中,符合查询模式的索引设计也会影响性能。

3. 模型定义

如果 Schema 是实际的数据库结构,则 Model 是要在应用程序代码中使用的对象。

// src/db/models/Post.js
import { Model } from '@nozbe/watermelondb'
import { field, children, writer } from '@nozbe/watermelondb/decorators'

export default class Post extends Model {
  static table = 'posts'

  static associations = {
    comments: { type: 'has_many', foreignKey: 'post_id' },
  }

  @field('title') title
  @field('body') body
  @field('is_pinned') isPinned

  @children('comments') comments

  @writer async updateTitle(title) {
    await this.update(post => {
      post.title = title
    })
  }
}
// src/db/models/Comment.js
import { Model } from '@nozbe/watermelondb'
import { field, relation } from '@nozbe/watermelondb/decorators'

export default class Comment extends Model {
  static table = 'comments'

  static associations = {
    posts: { type: 'belongs_to', key: 'post_id' },
  }

  @field('body') body
  @relation('posts', 'post_id') post
}

这里重要的一点是数据库更改不是在任何地方完成的。在WatermelonDB中,可以创建、修改、删除 database.write() 或者 @writer 从内部处理它。

4. 创建数据库

// src/db/index.js
import { Database } from '@nozbe/watermelondb'
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite'

import { mySchema } from './schema'
import Post from './models/Post'
import Comment from './models/Comment'

const adapter = new SQLiteAdapter({
  schema: mySchema,
})

export const database = new Database({
  adapter,
  modelClasses: [Post, Comment],
})

5. 数据创建与查询

// 글 생성
const newPost = await database.write(async () => {
  return await database.get('posts').create(post => {
    post.title = 'WatermelonDB 시작하기'
    post.body = 'React Native에서 로컬 DB를 다루는 방법'
    post.isPinned = false
  })
})
// 글 목록 조회
import { Q } from '@nozbe/watermelondb'

const pinnedPosts = await database
  .get('posts')
  .query(Q.where('is_pinned', true))
  .fetch()

6. 与React组件连接

这就是 WatermelonDB 的优势所在。如果将其连接到可观察对象,则屏幕会在数据更改时自动更新,而不仅仅是获取一次数据。

import React from 'react'
import { Text, View } from 'react-native'
import { withObservables } from '@nozbe/watermelondb/react'

function PostItem({ post }) {
  return (
    <View>
      <Text>{post.title}</Text>
      <Text>{post.body}</Text>
    </View>
  )
}

const enhance = withObservables(['post'], ({ post }) => ({
  post,
}))

export default enhance(PostItem)

列表还可以作为可观察量连接到查询。由于这种结构,减少了每次手动创建“数据库更改→状态更新→列表重新检查→屏幕更新”流程的需要。

SQLite 和 WatermelonDB 的比较

WatermelonDB 并不是一个完全取代 SQLite 的独立数据库,而是更接近于一个允许 SQLite 在 React Native 应用程序中以更高抽象的方式使用的工具。所以比较的标准不是“SQLite更好/WatermelonDB更好” 您需要什么级别的控制和生产力?全部。

物品 直接使用SQLite WatermelonDB
基本人格 本地关系数据库引擎/API 基于SQLite的响应式数据库框架
数据访问 编写自己的 SQL 使用模型、集合、查询 API
表现 如果你的查询设计得当的话会非常快 延迟加载+原生数据库查询+可观察结构
反应用户界面更新 需要直接的状态管理 可通过 observable 自动更新
关系数据 直接使用 SQL 连接/查询 提供关联和关系模型
迁移 设计你自己的 使用 WatermelonDB 迁移 API
同步 自己实施 提供同步原语和协议,但直接需要后端
运行曲线 如果您了解 SQL,则可以轻松访问 需要学习WatermelonDB规则以及装饰器和写入器模式
调试 基于DB文件/SQL的跟踪 WatermelonDB 抽象和本机构建问题必须一起看待
合适的应用程序 简单的本地存储和 SQL 控制很重要的应用程序 离线优先、高数据、自动 UI 刷新、以同步为中心的应用程序

什么时候直接使用SQLite更好

对于以下应用程序,无需引入WatermelonDB。

  • 没有太多数据可存储。
  • 仅保存简单的设置、缓存和最近的搜索词。
  • 自己编写和调优SQL更为重要。
  • 不需要自动连接到 React UI 的可观察结构。
  • 团队无法学习 WatermelonDB 的编写器/模型/同步规则。

在 React Native 中 expo-sqlite, react-native-sqlite-storage, op-sqlite, react-native-quick-sqlite 还有类似的选项。特别是如果它是一个基于世博会的项目。 expo-sqlite在安装和兼容性方面可能会更简单。

WatermelonDB 何时更适合

相反,如果满足以下条件,WatermelonDB就有优势。

  • 本地存储有数千到数万条数据。
  • 应用程序执行速度很重要。
  • 即使在离线状态下也必须创建/编辑数据。
  • 需要与服务器同步更改。
  • 必须从屏幕上的多个位置查看相同的数据并自动更新。
  • 我想围绕应用程序域模型而不是 SQL 来组织代码。

如果用一行来概括的话,就是这样的。

SQLite 是一个“本地数据库引擎”,而 WatermelonDB 是“用于 React Native 应用程序的基于 SQLite 的数据层”。

同步结构

WatermelonDB并不是一个只提供本地DB的工具。它还提供同步功能,并考虑到离线优先的应用程序。不过,这里有一点不应该被误解。

WatermelonDB 不会为您创建服务器。

根据官方文档,WatermelonDB 提供了以下内容:

  • 跟踪本地创建/编辑/删除的更改
  • synchronize() API
  • pullChanges, pushChanges 同步流程的形式
  • 更改后端必须适应的对象结构

在前端,它看起来大致是这样的。

import { synchronize } from '@nozbe/watermelondb/sync'

export async function syncDatabase(database) {
  await synchronize({
    database,
    pullChanges: async ({ lastPulledAt, schemaVersion, migration }) => {
      const response = await fetch(
        `https://api.example.com/sync?last_pulled_at=${lastPulledAt ?? ''}&schema_version=${schemaVersion}`
      )

      if (!response.ok) {
        throw new Error(await response.text())
      }

      const { changes, timestamp } = await response.json()
      return { changes, timestamp }
    },
    pushChanges: async ({ changes, lastPulledAt }) => {
      const response = await fetch(
        `https://api.example.com/sync?last_pulled_at=${lastPulledAt ?? ''}`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(changes),
        }
      )

      if (!response.ok) {
        throw new Error(await response.text())
      }
    },
    migrationsEnabledAtVersion: 1,
  })
}

后端必须按以下格式交换更改:

{
  "changes": {
    "posts": {
      "created": [
        { "id": "post_1", "title": "Hello", "body": "...", "is_pinned": false }
      ],
      "updated": [],
      "deleted": []
    },
    "comments": {
      "created": [],
      "updated": [],
      "deleted": ["comment_1"]
    }
  },
  "timestamp": 1710000000000
}

这里的关键是服务器时间戳和更改跟踪的一致性。官方文档也有pull端点 lastPulledAt 它解释了所有后续更改必须无一例外地返回,并且服务器时间必须设置在一致的基础上。如果这部分做得马虎,最终可能会出现“有些记录永远不会同步”的问题。

实际阻塞和解决方向

WatermelonDB 很强大,但它涉及到 React Native 原生模块、Babel、CocoaPods、Expo 和同步后端。实际开发过程中,经常会卡在下面这个点。

情况 1. 装饰器语法无法识别

症状通常如下所示:

SyntaxError: Support for the experimental syntax 'decorators' isn't currently enabled

首先,检查 Babel 设置。

npm ls @babel/plugin-proposal-decorators

.babelrc 或者 babel.config.js检查是否输入了遗留装饰器设置。

module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    ['@babel/plugin-proposal-decorators', { legacy: true }],
  ],
}

这里的一个常见错误是只查看 TypeScript 设置而忽略 Babel 设置。由于WatermelonDB示例使用了装饰器,因此必须在Metro/Babel管道中处理相应的语法。

案例 2. iOS 上 simdjson.h 或者,发生与 Pod 相关的构建错误。

在 iOS 构建中 simdjson 如果出现相关错误,请先检查Podfile和Pod安装状态。

cd ios
pod install

官方文档将指导您完成 Podfile。 simdjson 还要检查是否已输入设置。

pod 'simdjson', path: '../node_modules/@nozbe/simdjson', modular_headers: true

use_frameworks!使用的项目应该更加小心。 WatermelonDB官方安装文档中也给出了指导,大意是不推荐frameworks模式,并且相关的构建问题在GitHub issues中不断提及。该项目已经 use_frameworks!如果您依赖,那么在引入 WatermelonDB 之前首先在一个小示例分支上验证 iOS 构建会更安全。

案例 3. Expo Go 中未找到 Native 模块

Expo Go 不会随意包含任意原生模块。需要本机代码的库(例如 WatermelonDB)可能无法直接在 Expo Go 中运行。

示例症状类似于:

NativeModules.WMDatabaseBridge is not defined

在这种情况下,通常有三种选择。

1.使用Expo开发构建。 2. 检查配置插件或自定义本机设置。 3、如果需求简单 expo-sqlite将范围降低至

对于 Expo 项目,不要问“WatermelonDB 好用吗?”,而应该首先问“本机配置可以在当前的 Expo 工作流程中进行管理吗?”

案例4.React Native新架构兼容性不确定

React Native 0.7x之后,必须结合New Architecture、Bridgeless和最新的Expo SDK检查WatermelonDB的兼容性问题。与新架构、RN 0.76+ 和 Expo SDK 54+ 相关的疑问和问题发布在 GitHub issues 中。

如果是正在运行的app,建议按以下顺序检查。

npm view @nozbe/watermelondb version
npm view react-native version

另外,检查该项目的 RN/Expo 版本和 WatermelonDB GitHub 问题。在开启新架构之前,您必须首先验证本地数据库是否已正确初始化以及iOS/Android版本构建是否已完成。

案例 5. 重复或丢失同步

WatermelonDB同步并不仅仅以前端代码结束。后端 lastPulledAt 之后,必须准确返回更改。

如果出现问题,请检查以下内容:

  • 我什么时候获取服务器时间戳?
  • 是否有可能在拉取期间更改的数据可能会丢失?
  • 删除的记录是否会记录到 ID 列表中?
  • 您了解下次拉取时再次接收客户端推送的更改的结构吗?
  • 发生冲突时服务器必须采取什么策略来失败/允许?

在同步是关键的应用程序中,后端同步协议设计可能比引入 WatermelonDB 更重要。

何时使用它,何时避免它?

何时推荐 WatermelonDB

  • 我正在构建一个离线优先的应用程序。
  • 本地数据很多,应用程序执行速度很重要。
  • 需要一个关系数据模型。
  • 数据更改应自动反映在多个屏幕上。
  • 同步协议可以通过其自己的后端来实现。
  • 团队可以处理 React Native 原生构建问题。

何时避免或推迟使用 WatermelonDB

  • 只需要保存简单的设置。
  • 它仅短暂缓存服务器 API 响应。
  • 它必须仅使用Expo Go进行开发,并且本地构建配置很困难。
  • 我正在积极使用最新的 React Native 新架构,但我没有时间验证兼容性。
  • 有许多复杂的分析查询需要直接控制 SQL。
  • 我无力自己设计同步后端。

结论

如果你把 WatermelonDB 的核心视为“一个让在 React Native 中使用 SQLite 更方便的库”,那就有点欠缺了。更准确地说 考虑大量本地数据、响应式 UI 和离线优先同步的数据框架。全部。

与 SQLite 相比,WatermelonDB 放弃了一些直接控制,但获得了应用程序级功能,例如模型驱动开发、可观察的 UI 更新和同步原语。因此,对于需要简单存储的应用程序来说,它是大材小用,但对于即使离线时数据仍不断积累和变化的应用程序来说,它非常适合。

如果你正在React Native项目中考虑WatermelonDB,你可以先这样判断。

1. 真的会有很多数据吗? 2. 是否需要离线创作/编辑? 3.我需要与服务器同步吗? 4. 您有时间验证 Expo/RN/iOS/Android 构建兼容性吗? 5. 应用程序域模型和响应式 UI 是否比直接控制 SQL 更重要?

如果您对大多数问题的回答是“是”,那么您可能想尝试一下 WatermelonDB。相反,如果只有两个或三个适用,则首先 expo-sqliteop-sqlite 简单地从相同的 SQLite 库开始并在数据结构增长时迁移到 WatermelonDB 的策略也是现实的。

参考资料