Help us shape the future of Go! Take the 2026 Go Developer Survey

dbmigrate

package
v0.32.41 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 31, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package dbmigrate 换库(SQLite → Postgres/MySQL):把一个实例的数据**逻辑**搬到另一个库。

为什么是「逻辑导出/导入」而不是「物理拷贝」

备份包(app/backup)做的是**物理快照**:SQLite 的 `VACUUM INTO` 产出一个 `.db` 文件,恢复时把它**换回去**。⇒ 这条路只在 SQLite → SQLite 之间成立: **Postgres/MySQL 根本读不了 SQLite 的文件格式**,反之亦然。

而「换库」的真需求恰恰是跨方言(SQLite → PG/MySQL)⇒ 必须走**逻辑**路线:

导出:把每张表读成**行数据**(与方言无关的 JSON)
导入:目标库按**目标方言**建表(GORM AutoMigrate)→ 逐行插入

⇒ 源库与目标库的方言在整条链路上**解耦**:SQLite→PG、PG→MySQL、MySQL→SQLite 全部同一条代码路径。

三条语义(缺一条就危险)

  1. **取件码与分享链接不变**:行数据**原样搬运**(含 `file_codes.id` 与 `code`) ⇒ 用户的对外名片照旧可用。
  2. **导入是破坏性操作**:它会写目标库 ⇒ 必须**显式确认**。
  3. **导入后自增必须对齐**:导入是**显式写 ID**(为了让主键与外键保持不变), 而 PG 的 `bigserial` 序列 / MySQL 的 `AUTO_INCREMENT` **不会**因为显式插入 而前进 ⇒ 不重置的话,**下一次新建行立刻撞主键**。 ⇒ 这是换库最经典的静默地雷 ⇒ 本包把「序列对齐」做成**必经步骤**, 并有专门的守卫用例(见 import_test.go)。

不搬什么

  • `schema_migrations`:版本记录是**方言相关**的(SQLite 的版本号与 PG 的迁移集 没有对应关系)⇒ 目标库用 AutoMigrate 建表(方言无关),不搬版本记录。
  • 存储对象:对象在**文件系统/对象存储**里,与数据库无关 ⇒ 换库不涉及。

Index

Constants

View Source
const DumpName = "pigeonbox-db-dump.json"

DumpName 逻辑导出文件(JSON)的建议文件名后缀。

View Source
const FormatVersion = 1

FormatVersion 逻辑导出格式版本。

与备份包(app/backup)**各自独立**地演进:备份包是「物理快照 + 对象」, 本格式是「纯行数据」⇒ 两者的兼容性问题互不相干。

Variables

View Source
var ErrImportNeedsConfirm = fmt.Errorf("导入会写入目标数据库,属破坏性操作:须显式确认")

ErrImportNeedsConfirm 导入未显式确认(会写目标库 ⇒ 破坏性操作必须显式确认)。

View Source
var ErrUnsupportedDumpVersion = fmt.Errorf("不认识的数据库导出格式版本")

ErrUnsupportedDumpVersion 不认识的导出格式版本(宁可报错也不猜着还原)。

Functions

This section is empty.

Types

type Dump

type Dump struct {
	// DumpFormat 格式版本(见 FormatVersion)。
	DumpFormat int `json:"dump_format"`
	// CreatedAt 导出时间(RFC3339)。
	CreatedAt string `json:"created_at"`
	// SourceDriver 源库方言(sqlite / postgres / mysql)。
	SourceDriver string `json:"source_driver"`
	// Tables 表数据(顺序 = `model.Tables()` 的登记顺序 ⇒ 父表在前)。
	Tables []TableDump `json:"tables"`
}

Dump 一次逻辑导出的完整结果。

func ReadDump

func ReadDump(r io.Reader) (*Dump, error)

ReadDump 从 JSON 读回一个 dump(校验格式版本)。

func (*Dump) RowCount

func (d *Dump) RowCount(table string) int

RowCount 返回 dump 里某表的行数(表不存在 ⇒ 0)。

func (*Dump) TableNames

func (d *Dump) TableNames() []string

TableNames 返回 dump 里包含的表名。

func (*Dump) TotalRows

func (d *Dump) TotalRows() int

TotalRows 返回全部表的行数合计。

func (*Dump) WriteJSON

func (d *Dump) WriteJSON(w io.Writer) error

WriteJSON 把 dump 序列化成 JSON。

type Exporter

type Exporter struct {
	// contains filtered or unexported fields
}

Exporter 逻辑导出器。

func NewExporter

func NewExporter(db *gorm.DB) *Exporter

NewExporter 创建导出器(源库句柄)。

func (*Exporter) Export

func (e *Exporter) Export(ctx context.Context) (*Dump, error)

Export 把源库的**全部领域模型表**导出成与方言无关的行数据。

为什么按「模型」而不是「表名」导

逐表硬编码列名 ⇒ 每加一个字段就要改这里 ⇒ 迟早漂移(漏字段 = 换库后数据丢失)。 ⇒ 这里用 **GORM 的 schema 反射**从模型推出「列名」, 导出时按 schema 的列顺序取值 ⇒ **模型改了,这里自动跟着改**。

值怎么变成字符串

取原始值后按 Go 侧类型归一化成字符串:

· bool   → "true" / "false"
· 整数   → 十进制字符串(**不经 float** ⇒ 不丢精度)
· 时间   → RFC3339Nano
· []byte → 原样当字符串(TEXT 列)
· nil   → ""(导入侧由列的 nullable 语义区分 NULL 与空串)

type ImportReport

type ImportReport struct {
	// Tables 逐表结果(表名 + 导入行数)。
	Tables []TableImportResult
	// TotalRows 导入行数合计。
	TotalRows int
	// SequencesAligned 已对齐自增的表名清单。
	SequencesAligned []string
}

ImportReport 一次导入的结果。

type Importer

type Importer struct {
	// contains filtered or unexported fields
}

Importer 逻辑导入器(把行数据写进目标库)。

func NewImporter

func NewImporter(db *gorm.DB, confirm bool) *Importer

NewImporter 创建导入器(目标库句柄)。

⚠️ `confirm=false` ⇒ 任何写操作都会失败(ErrImportNeedsConfirm)。

func (*Importer) Import

func (i *Importer) Import(ctx context.Context, dump *Dump) (*ImportReport, error)

Import 把 dump 的行数据导入目标库。

顺序

  1. **建表**:用 `model.AutoMigrateModels()` 在目标库按**目标方言**建表 (方言无关 ⇒ 不依赖那些 SQLite 方言的 migrations/*.sql)。
  2. **逐表导入**:按 dump 里的表顺序(= 登记顺序 ⇒ 父表在前 ⇒ 外键不炸)。
  3. **序列对齐**:把目标库的自增计数器重置到 `max(id)+1` ⇒ **不做这一步,下一次新建行立刻撞主键**(PG 的 bigserial 序列 与 MySQL 的 AUTO_INCREMENT 都不会因为显式插入 ID 而前进)。

幂等/可中断

每张表独立导入 ⇒ 失败时**已导入的表保持已导入**(可重跑;重跑前需清空目标表)。

type TableDump

type TableDump struct {
	// Name 表名。
	Name string `json:"name"`
	// Columns 列名(按值顺序一一对应)。
	Columns []string `json:"columns"`
	// Rows 行数据;每行是 `Columns` 下标的**值数组**。
	//
	// ⚠️ 为什么用**字符串**而不是原生 JSON 值:
	// 数据库的值类型跨方言差异大(SQLite 的 `numeric` 布尔 vs PG 的 `boolean`;
	// SQLite 的 `datetime` 文本 vs PG 的 `timestamptz`)⇒ 用字符串承载「原始文本」,
	// 由**导入侧按目标列类型**归一化(见 import.go 的 normalizeValue)。
	// ⇒ 导出/导入两侧都不丢精度(整数不会被 JSON 浮点化)。
	//
	// ⚠️ **NULL 与空串必须可区分**:`user_id` 这类可空指针列,NULL(匿名上传)
	// 与空串是**不同语义** ⇒ 值用 `*string` 承载(JSON `null` vs `"…"`)。
	// (第一版用 `[]string` + 空串表示 NULL ⇒ 导入时把 NULL 变成了空串
	// ⇒ PG 的 `integer` 列收到 `""` 直接报错 —— 真机上必然踩。)
	Rows [][]*string `json:"rows"`
}

TableDump 一张表的行数据。

type TableImportResult

type TableImportResult struct {
	// Name 表名。
	Name string
	// Rows 导入的行数。
	Rows int
}

TableImportResult 单表导入结果。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL