- 后端
- 数据库
- 文档数据库
【免费下载链接】FerretDB
A truly Open Source MongoDB alternative
本文讲解如何以 Ubicloud Managed Postgres 作为后端存储,为 PostgreSQL 数据库叠加 MongoDB 兼容层 FerretDB,并通过一个基于 Flask 与 pymongo 的联系人管理应用完成插入、查询、更新、删除的端到端验证。读完本文,你将掌握 FerretDB 与托管 Postgres 的完整对接流程、mongosh连接认证方式,以及如何在 psql 中直接检视 FerretDB 写入 Postgres 的 JSONB 数据。
FerretDB 与 Ubicloud Managed Postgres:为什么组合在一起
FerretDB 是一个开源文档数据库,它的核心定位是为 Postgres、SQLite 等关系型数据库后端“叠加”MongoDB 兼容能力:应用继续使用 MongoDB 协议与 API,数据却由成熟的 Postgres 存储与保障。这一组合在数据库成本与供应商锁定(vendor lock-in)问题上提供了更灵活的选择——你不需要被托管 MongoDB 服务的计价模型绑住,数据始终掌握在自己手中。
Ubicloud 是开放、可移植的云平台,其 Managed Postgres 服务提供自动备份、时间点恢复(point-in-time restore)、每台 Postgres 服务器独立的专用虚拟机,以及传输中和静态数据的加密。将 FerretDB 指向 Ubicloud Managed Postgres 后,你得到的是一个“可在任何 Postgres 上迁移的 MongoDB 兼容数据库”,这正是原文档强调的核心价值。
版本提示:原文档撰写于 2024 年 1 月,当时 FerretDB 运行版本为 v1.18.0(后端为 PostgreSQL 16.1),直接对接普通 Postgres 即可。当前仓库(v2 系列)已演进为通过 PostgreSQL 的 DocumentDB 扩展提供服务,且客户端认证仅支持
SCRAM-SHA-256。本文保留原文档的完整实操流程,并在对应小节标注当前仓库的差异,以便两类用户都能按图索骥。
准备工作(Prerequisites)
开始前请确保具备以下条件:
- Ubicloud Postgres 连接 URI(连接字符串);
psql命令行工具;- Docker(用于本地运行 FerretDB 容器);
mongosh(MongoDB Shell,用于连接 FerretDB)。
在 Ubicloud 上创建 Postgres 实例
FerretDB 需要一个 Postgres 连接字符串作为后端。按照 Ubicloud 的托管 Postgres 快速入门文档创建实例后,你会得到默认postgres用户的连接字符串,格式如下:
postgres://postgres:<password>@<host address>接下来,为 FerretDB 创建独立的数据库与专用账号。使用 psql 连接 Ubicloud Postgres:
psql <ubicloud-postgres-connection-string>然后在 psql 中执行:
CREATE USER ferretuser WITH PASSWORD <password>; CREATE DATABASE ferretdb OWNER ferretuser; GRANT ALL PRIVILEGES ON DATABASE ferretdb TO ferretuser;这里创建的ferretuser账号与ferretdb数据库将贯穿后续所有步骤。为数据库建立专用账号而不是直接使用超级用户,符合最小权限原则,也让FERRETDB_POSTGRESQL_URL中的凭据边界清晰。
运行 FerretDB:连接字符串如何配置
FerretDB 通过 PostgreSQL 连接 URL 定位后端数据库。在当前仓库中,该参数对应的配置项定义在 配置文档:
| Flag | 说明 | 环境变量 | 默认值 |
|---|---|---|---|
--postgresql-url | PostgreSQL 连接 URL | FERRETDB_POSTGRESQL_URL | postgres://127.0.0.1:5432/postgres |
--postgresql-url-file | 指向包含连接 URL 的文件;非空时覆盖--postgresql-url | FERRETDB_POSTGRESQL_URL_FILE | (空) |
每个命令行 Flag 都有对应的环境变量等价形式,方便容器化与云原生部署。FerretDB 使用 pgx v5 库连接 PostgreSQL,仓库文档同时注明:pool_min_conns未设置时默认 10(覆盖 pgx 的 0)、pool_max_conns默认 50(覆盖 pgx 的 4)、application_name恒为FerretDB、timezone恒为UTC——这些默认值意味着托管实例的连接池配额至少要能容纳 FerretDB 的并发需求。
现在将ferretuser的连接字符串整理为如下格式:
postgres://ferretuser:<password>@<postgres-server-hostname>/ferretdb在终端中拉取并运行 FerretDB 镜像,把连接字符串通过FERRETDB_POSTGRESQL_URL环境变量注入容器:
docker run -e FERRETDB_POSTGRESQL_URL=<ferretuser-connection-string> ghcr.io/ferretdb/ferretdb若希望从宿主机访问,可参照同类部署实践追加端口映射(如-p 27017:27017),这样mongosh即可通过127.0.0.1:27017访问容器内的 FerretDB。
使用 mongosh 连接并验证版本
FerretDB 启动成功后,用mongosh连接实例。原文档写作时 FerretDB 支持 PLAIN 认证机制,因此需要在 MongoDB URI 中显式声明:
mongosh 'mongodb://<ferretuser>:<ferretuser-password>@127.0.0.1:27017/ferretdb?authMechanism=PLAIN'连接成功后可以看到类似如下的启动信息(原文档基于 FerretDB v1.18.0 与 PostgreSQL 16.1):
Current Mongosh Log ID: 65afa52615e82bd1fc9d4371 Connecting to: mongodb://<credentials>@127.0.0.1:27018/ferretdb?authMechanism=PLAIN&directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.1.0 Using MongoDB: 7.0.42 Using Mongosh: 2.1.0 ------ The server generated these startup warnings when booting 2024-01-23T11:38:17.837Z: Powered by FerretDB v1.18.0 and PostgreSQL 16.1. ... ferretdb>当前仓库的认证机制差异
需要特别说明的是,认证机制的实现随版本演进已发生变化。当前仓库中,客户端认证仅支持SCRAM-SHA-256机制:
saslStart/saslContinue命令注册于 内部命令表,分发入口位于 cmd_query.go;- 在 msg_saslstart.go 中,若
mechanism不是SCRAM-SHA-256,服务端会直接返回机制不可用错误; - 认证流程为 SCRAM 握手:解析客户端首条消息 → 从 Postgres 查询用户的 salt 与迭代次数 → 生成服务端首条消息返回,最终通过
saslContinue完成握手。
因此,在 v2 系列上部署时,应改用 SCRAM 兼容的认证方式连接,例如直接使用带用户凭据的 URI(默认即 SCRAM-SHA-256)而不显式指定authMechanism=PLAIN。完整的认证流程说明见 认证文档:FerretDB 自身不存储任何用户凭据,而是把客户端凭据转发给 PostgreSQL 校验;匿名客户端虽可建立连接,但无法访问数据库。
用 Python 联系人应用做端到端测试
进入ferretdb>提示符后,即可按 MongoDB 习惯操作数据。原文档使用一个 Flask 联系人应用(基于pymongo)验证基本 CRUD,下面的代码与结构可直接照搬。
搭建项目结构
mkdir ContactApp cd ContactApp touch app.py mkdir templates touch templates/index.html templates/update.html首页模板 index.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Contact Book</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css" /> <script> function confirmDelete(contact_id) { if (confirm('Are you sure you want to delete this contact?')) { document.getElementById('delete-form-' + contact_id).submit() } } </script> </head> <body class="ui container"> <h1>Contact Book</h1> <form action="/add" method="POST" class="ui form"> <div class="ui form"> <div class="four fields"> <div class="field"> <label>Name</label> <input type="text" name="name" placeholder="Name" required /> </div> <div class="field"> <label>Phone</label> <input type="tel" name="phone" placeholder="Phone" /> </div> <div class="field"> <label>Email</label> <input type="email" name="email" placeholder="Email" /> </div> </div> <button class="ui green button" type="submit">Add Contact</button> </div> </form> <table class="ui celled table"> <thead> <tr> <th>Name</th> <th>Phone</th> <th>Email</th> <th>Actions</th> </tr> </thead> <tbody> {% for contact in contacts %} <tr> <td>{{ contact['name'] }}</td> <td>{{ contact['phone'] }}</td> <td>{{ contact['email'] }}</td> <td> <a href="/update/{{ contact['_id'] }}" class="ui yellow button" >Update</a > <form id="delete-form-{{ contact['_id'] }}" action="/delete/{{ contact['_id'] }}" method="post" style="display: inline;" > <button type="button" onclick="confirmDelete('{{ contact['_id'] }}')" class="ui red button" > Delete </button> </form> </td> </tr> {% endfor %} </tbody> </table> </body> </html>更新页模板 update.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Contact Book</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css"> </head> <body class="ui container"> <h1>Update Contact</h1> <form action="/update/{{ contact['_id'] }}" method="post" class="ui form"> <div class="ui form"> <div class="four fields"> <div class="field"> <input type="text" name="name" value="{{ contact['name'] }}" placeholder="Name" required> </div> <div class="field"> <input type="tel" name="phone" value="{{ contact['phone'] }}" placeholder="Phone"> </div> <div class="field"> <input type="email" name="email" value="{{ contact['email'] }}" placeholder="Email"> </div> <button type="submit" class="ui green button">Update Contact</button> </div> </form> </div> </body> </html>应用主逻辑 app.py
import os from flask import Flask, render_template, request, redirect, url_for from pymongo import MongoClient from bson.objectid import ObjectId app = Flask(__name__) mongo_uri = os.getenv('MONGO_URI', 'mongodb://localhost:27017/') client = MongoClient(mongo_uri) db = client.ferretdb contacts_collection = db.contacts @app.route('/') def index(): contacts = contacts_collection.find() return render_template('index.html', contacts=contacts) @app.route('/add', methods=['POST']) def add_contact(): try: name = request.form.get('name') phone = request.form.get('phone') email = request.form.get('email') contacts_collection.insert_one({'name': name, 'phone': phone, 'email': email}) except Exception as e: message = f"An error occurred: {e}" return redirect(url_for('index')) @app.route('/delete/<contact_id>', methods=['POST']) def delete_contact(contact_id): try: contacts_collection.delete_one({'_id': ObjectId(contact_id)}) except Exception as e: message = f"An error occurred while deleting the contact: {e}" return redirect(url_for('index')) @app.route('/update/<contact_id>', methods=['GET', 'POST']) def update_contact(contact_id): contact = contacts_collection.find_one({'_id': ObjectId(contact_id)}) if request.method == 'POST': try: updated_data = { 'name': request.form.get('name'), 'phone': request.form.get('phone'), 'email': request.form.get('email') } contacts_collection.update_one({'_id': ObjectId(contact_id)}, {'$set': updated_data}) except Exception as e: message = f"An error occurred while updating the contact: {e}" return redirect(url_for('index')) return render_template('update.html', contact=contact) if __name__ == '__main__': app.run(debug=True)这段代码展示了 MongoDB 文档模型的关键体验:insert_one、find、find_one、update_one、delete_one等操作对应用透明,FerretDB 在背后把它们翻译为对 Postgres 的写入——这正是“为关系型后端叠加 MongoDB 兼容层”的含义。
运行应用
先通过环境变量注入 MongoDB 连接字符串,再启动应用:
export MONGO_URI=mongodb://<mongodb-URI> python app.py向应用添加以下联系人:
James McArthur 093465729276 jamesmcarthur@yahoo.com Desmond Eko 064357692721 eko@gmail.com Christine Elle 046899553291 christianelle@yahoo.com随后可以验证更新与删除功能:例如通过更新按钮把Desmond Eko改为Andrew Eko,或删除列表中的某条记录,观察数据在页面上与 Postgres 中的同步变化。
在 psql 中检视数据:FerretDB 的 JSONB 存储
FerretDB 把 MongoDB 文档存储为 Postgres 中的 JSONB 数据。使用 Ubicloud 的 Postgres 连接字符串连接 psql,将搜索路径切到ferretdbschema 后即可查看:
psql <postgres-connection-string>set search_path to ferretdb;ferretdb=> \dt List of relations Schema | Name | Type | Owner -------------+-----------------------------+-------+------------ ferretdb | _ferretdb_database_metadata | table | ferretuser ferretdb | contacts_cedcb8f0 | table | ferretuser (2 rows) ferretdb=> table contacts_cedcb8f0; _jsonb --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- {"$s": {"p": {"_id": {"t": "objectId"}, "name": {"t": "string"}, "email": {"t": "string"}, "phone": {"t": "string"}}, "$k": ["_id", "name", "phone", "email"]}, "_id": "65afb5c8f1f80562d49e2076", "name": "James McArthur", "email": "jamesmcarthur@yahoo.com", "phone": "093465729276"} {"$s": {"p": {"_id": {"t": "objectId"}, "name": {"t": "string"}, "email": {"t": "string"}, "phone": {"t": "string"}}, "$k": ["_id", "name", "phone", "email"]}, "_id": "65afb655f1f80562d49e2078", "name": "Christine Elle\t", "email": "christianelle@yahoo.com", "phone": "046899553291"} {"$s": {"p": {"_id": {"t": "objectId"}, "name": {"t": "string"}, "email": {"t": "string"}, "phone": {"t": "string"}}, "$k": ["_id", "name", "phone", "email"]}, "_id": "65afb5f1f1f80562d49e2077", "name": "Andrew Eko", "email": "eko@gmail.com", "phone": "064357692721"} (3 rows) ferretdb=>观察这个输出,可以提取出几个关键事实:
- 每个集合对应一张 Postgres 表(此处为
contacts_cedcb8f0),FerretDB 维护一张_ferretdb_database_metadata元数据表; - 整条 MongoDB 文档存放在单一
_jsonb列中,文档内的_id保留为 ObjectId 类型(形如65afb5c8f1f80562d49e2076); - JSONB 中带有
$sschema 描述($p为字段类型映射、$k为键顺序),这正是 FerretDB 能够在关系型存储上还原 MongoDB 文档语义的实现细节; - 更新操作(
Desmond Eko→Andrew Eko)与删除操作都已正确反映在 Postgres 数据中。
总结
FerretDB 为 Ubicloud Managed Postgres 提供了一条低成本的 MongoDB 兼容路径:数据由托管 Postgres 负责可靠性、备份与恢复,应用侧则继续使用 MongoDB 协议与工具链。这套组合尤其适合已经运行在 Hetzner 数据中心、需要托管 Postgres 服务,同时希望保留 MongoDB 开发体验、又不想被供应商锁定的团队。
需要再次提醒的版本差异:原文档基于 v1.18.0(直接对接普通 Postgres、支持 PLAIN 认证);当前仓库 v2 系列要求 Postgres 安装 DocumentDB 扩展,客户端认证仅支持 SCRAM-SHA-256。部署前请先阅读仓库中的 认证文档、配置参数文档 与 Docker 安装文档,根据实际版本选择匹配的连接方式与认证参数。
- 后端
- 数据库
- 文档数据库
【免费下载链接】FerretDB
A truly Open Source MongoDB alternative
相关推荐
使用 pgEdge 分布式 PostgreSQL 作为后端运行 FerretDB:从 Docker 部署到 MongoDB CRUD 实战
使用 pgEdge 分布式 PostgreSQL 作为后端运行 FerretDB:从 Docker 部署到 MongoDB CRUD 实战 FerretDB 是
后端数据库文档数据库使用 Airflow 构建端到端数据管道:从 CSV 下载到 Postgres 清洗入库的完整实战
使用 Airflow 构建端到端数据管道:从 CSV 下载到 Postgres 清洗入库的完整实战 本文是 Apache Airflow 入门系列教程的第三篇,
后端任务调度工作流自动化数据编排批处理数据工程流程编排使用 Electric 对接 Supabase:从托管 Postgres 到 Edge Function 的端到端同步指南
使用 Electric 对接 Supabase:从托管 Postgres 到 Edge Function 的端到端同步指南 导读 本指南以 Electric 官
后端数据同步数据库人工智能AI AgentMCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考