byjg/migration

PHP数据库版本控制简单库。支持Sqlite、MySql、Sql Server和Postgres。

资助包维护!
byjg

4.9.1 2024-01-05 20:21 UTC

README

Opensource ByJG GitHub source GitHub license GitHub release Scrutinizer Code Quality Build Status

功能

这是一个用于数据库版本控制的PHP简单库。目前支持Sqlite、MySql、Sql Server和Postgres。

数据库迁移可以用作

  • 命令行界面
  • PHP库,可集成到您的功能测试中
  • 无论您的编程语言或框架如何,都可以集成到您的CI/CD中。

数据库迁移仅使用SQL命令来对数据库进行版本控制。

为什么使用纯SQL命令?

大多数框架倾向于使用编程语句来对数据库进行版本控制,而不是使用纯SQL。

使用框架的本地编程语言来维护数据库有一些优点

  • 框架命令有一些用于执行复杂任务的技巧代码;
  • 您可以一次性编码并将代码部署到不同的数据库系统中;
  • 还有其他

但是,尽管这些功能很好,在大项目中,有人最终会使用MySQL Workbench来更改数据库,然后花费数小时将代码翻译成PHP。那么,为什么不用MySQL Workbench、JetBrains DataGrip等现有的功能,这些功能提供了更新数据库所需的SQL命令,并将其直接放入数据库版本控制系统中呢?

因为这个项目是中立的(独立于框架和编程语言),并使用纯SQL命令来迁移数据库。

安装

PHP库

如果您只想在项目中使用PHP库

composer require "byjg/migration"

命令行界面

命令行界面是独立的,不需要您与项目一起安装。

您可以全局安装并创建一个符号链接

composer require "byjg/migration-cli"

请访问 byjg/migration-cli 以获取有关Migration CLI的更多信息。

支持的数据库

它是如何工作的?

数据库迁移使用纯SQL来管理数据库版本。为了使其工作,您需要

  • 创建SQL脚本
  • 使用命令行或API进行管理。

SQL脚本

脚本分为三组

  • BASE脚本包含创建全新数据库的所有SQL命令;
  • UP脚本包含所有将数据库版本升级的SQL迁移命令;
  • DOWN脚本包含所有将数据库版本降级或回滚的SQL迁移命令;

脚本目录是

 <root dir>
     |
     +-- base.sql
     |
     +-- /migrations
              |
              +-- /up
                   |
                   +-- 00001.sql
                   +-- 00002.sql
              +-- /down
                   |
                   +-- 00000.sql
                   +-- 00001.sql
  • "base.sql" 是基础脚本
  • "up" 文件夹包含将版本升级的脚本。例如:00002.sql 是将数据库从版本 '1' 升级到 '2' 的脚本。
  • "down" 文件夹包含将版本降级的脚本。例如:00001.sql 是将数据库从版本 '2' 降级到 '1' 的脚本。 "down" 文件夹是可选的。

多开发环境

如果您与多个开发人员和多个分支一起工作,那么确定下一个数字就非常困难。

在这种情况下,版本号后面应添加 "-dev" 后缀。

请看以下场景

  • 开发人员1创建了一个分支,最新的版本号是例如42。
  • 开发人员2同时创建了一个分支,并且具有相同的数据库版本号。

在这种情况下,两个开发人员都会创建一个名为 43-dev.sql 的文件。两个开发人员都可以毫无问题地迁移UP和DOWN,并且您的本地版本将是43。

但是开发者1合并了您的更改并创建了最终版本43.sql(git mv 43-dev.sql 43.sql)。如果开发者2更新您的本地分支,他将会有一个文件43.sql(来自开发者1)和您的文件43-dev.sql。如果他在尝试迁移UP或DOWN时,迁移脚本将会失败并提醒他存在两个版本43。在这种情况下,开发者2将不得不更新您的文件到44-dev.sql并继续工作,直到合并您的更改并生成最终版本。

使用PHP API并将其集成到您的项目中

基本用法是

  • 创建一个连接,一个ConnectionManagement对象。更多信息请参阅“byjg/anydataset”组件
  • 使用此连接以及存放sql脚本的文件夹创建一个Migration对象。
  • 使用适当的命令“重置”、“UP”或“DOWN”迁移脚本。

请看一个例子

<?php
// Create the Connection URI
// See more: https://github.com/byjg/anydataset#connection-based-on-uri
$connectionUri = new \ByJG\Util\Uri('mysql://migrateuser:migratepwd@localhost/migratedatabase');

// Register the Database or Databases can handle that URI:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);

// Create the Migration instance
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');

// Add a callback progress function to receive info from the execution
$migration->addCallbackProgress(function ($action, $currentVersion, $fileInfo) {
    echo "$action, $currentVersion, ${fileInfo['description']}\n";
});

// Restore the database using the "base.sql" script
// and run ALL existing scripts for up the database version to the latest version
$migration->reset();

// Run ALL existing scripts for up or down the database version
// from the current version until the $version number;
// If the version number is not specified migrate until the last database version
$migration->update($version = null);

Migration对象控制数据库版本。

在您的项目中创建版本控制

<?php
// Register the Database or Databases can handle that URI:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);

// Create the Migration instance
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');

// This command will create the version table in your database
$migration->createVersion();

获取当前版本

<?php
$migration->getCurrentVersion();

添加回调以控制进度

<?php
$migration->addCallbackProgress(function ($command, $version, $fileInfo) {
    echo "Doing Command: $command at version $version - ${fileInfo['description']}, ${fileInfo['exists']}, ${fileInfo['file']}, ${fileInfo['checksum']}\n";
});

获取Db Driver实例

<?php
$migration->getDbDriver();

要使用它,请访问:https://github.com/byjg/anydataset-db

避免部分迁移(MySQL不支持)

部分迁移是指迁移脚本在过程中由于错误或手动中断而中断。

迁移表将处于部分UP部分DOWN状态,需要手动修复后才能再次迁移。

为了避免这种情况,您可以指定迁移将在事务性上下文中运行。如果迁移脚本失败,事务将回滚,迁移表将标记为完成,版本将是导致错误的脚本之前的立即前一个版本。

要启用此功能,您需要调用方法withTransactionEnabled并将true作为参数传递

<?php
$migration->withTransactionEnabled(true);

注意:此功能在MySQL中不可用,因为它不支持事务中的DDL命令。如果您使用此方法与MySQL,迁移将静默忽略它。更多信息:https://dev.mysqlserver.cn/doc/refman/8.0/en/cannot-roll-back.html

Postgres SQL迁移的技巧

关于创建触发器和SQL函数

-- DO
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Check that empname and salary are given
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname cannot be null'; -- it doesn't matter if these comments are blank or not
        END IF; --
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% cannot have null salary', NEW.empname; --
        END IF; --

        -- Who works for us when they must pay for it?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% cannot have a negative salary', NEW.empname; --
        END IF; --

        -- Remember who changed the payroll when
        NEW.last_date := current_timestamp; --
        NEW.last_user := current_user; --
        RETURN NEW; --
    END; --
$emp_stamp$ LANGUAGE plpgsql;


-- DON'T
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Check that empname and salary are given
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname cannot be null';
        END IF;
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% cannot have null salary', NEW.empname;
        END IF;

        -- Who works for us when they must pay for it?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
        END IF;

        -- Remember who changed the payroll when
        NEW.last_date := current_timestamp;
        NEW.last_user := current_user;
        RETURN NEW;
    END;
$emp_stamp$ LANGUAGE plpgsql;

由于PDO数据库抽象层无法运行SQL语句的批处理,当byjg/migration读取迁移文件时,必须将整个SQL文件的全部内容在分号处分割,并逐条执行语句。然而,有一种语句可以在其主体中包含多个分号:函数。

为了能够正确解析函数,byjg/migration 2.1.0开始根据分号 + EOL序列分割迁移文件,而不是仅仅分号。这样,如果您在每个函数定义的内部分号后添加一个空注释,byjg/migration将能够解析它。

不幸的是,如果您忘记添加任何这些注释,库将把CREATE FUNCTION语句分割成多个部分,迁移将失败。

避免冒号字符(:

-- DO
CREATE TABLE bookings (
  booking_id UUID PRIMARY KEY,
  booked_at  TIMESTAMPTZ NOT NULL CHECK (CAST(booked_at AS DATE) <= check_in),
  check_in   DATE NOT NULL
);


-- DON'T
CREATE TABLE bookings (
  booking_id UUID PRIMARY KEY,
  booked_at  TIMESTAMPTZ NOT NULL CHECK (booked_at::DATE <= check_in),
  check_in   DATE NOT NULL
);

由于PDO使用冒号字符作为预处理语句中命名参数的前缀,其在其他上下文中的使用将导致它出错。

例如,PostgreSQL语句可以使用::在类型之间进行值转换。另一方面,PDO将读取这作为在无效上下文中的无效命名参数,并在尝试运行时失败。

唯一修复这种不一致的方法是完全避免冒号(在这种情况下,PostgreSQL还有另一种语法:CAST(value AS type))。

使用SQL编辑器

最后,编写手动SQL迁移可能很繁琐,但使用能够理解SQL语法、提供自动完成、检查当前数据库架构或自动格式化代码的编辑器会容易得多。

处理一个模式内部的多种迁移

如果您需要在同一模式内创建不同的迁移脚本和版本,虽然技术上可行,但风险极高,我 强烈不建议 使用此方法。

为此,您需要通过传递参数给构造函数来创建不同的“迁移表”。

<?php
$migration = new \ByJG\DbMigration\Migration("db:/uri", "/path", true, "NEW_MIGRATION_TABLE_NAME");

出于安全考虑,此功能在命令行不可用,但您可以使用环境变量 MIGRATION_VERSION 来存储名称。

我们真心建议不要使用此功能。建议每个模式使用一个迁移。

运行单元测试

基本单元测试可以通过以下方式运行

vendor/bin/phpunit

运行数据库测试

运行集成测试需要确保数据库已启动并运行。我们提供了一个基本的 docker-compose.yml 文件,您可以使用它来启动数据库进行测试。

运行数据库

docker-compose up -d postgres mysql mssql

运行测试

vendor/bin/phpunit
vendor/bin/phpunit tests/SqliteDatabase*
vendor/bin/phpunit tests/MysqlDatabase*
vendor/bin/phpunit tests/PostgresDatabase*
vendor/bin/phpunit tests/SqlServerDblibDatabase*
vendor/bin/phpunit tests/SqlServerSqlsrvDatabase*

可选地,您可以设置单元测试使用的服务器和密码

export MYSQL_TEST_HOST=localhost     # defaults to localhost
export MYSQL_PASSWORD=newpassword    # use '.' if want have a null password
export PSQL_TEST_HOST=localhost      # defaults to localhost
export PSQL_PASSWORD=newpassword     # use '.' if want have a null password
export MSSQL_TEST_HOST=localhost     # defaults to localhost
export MSSQL_PASSWORD=Pa55word
export SQLITE_TEST_HOST=/tmp/test.db      # defaults to /tmp/test.db

相关项目

依赖项

开源 ByJG