dbmigrate

package
v0.33.2 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 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