在环境之间同步WordPress数据库更改:我们如何处理合并

full merging database featured img

数据库同步是一个在我所在的本地WordPress社区几乎每个月都会提出的问题——“如果我在本地站点进行更改,如果将本地数据库推回生产环境,生产站点上的任何更新会怎样?”

在本文中,我将重新审视这个问题,探讨您有哪些选择,分享我们如何处理我们网站的这个问题,并回顾现有的现成解决方案。

问题是什么?

这几乎是任何在本地开发副本上工作且同时运行着实时版本的WordPress网站的开发者都会遇到的问题。开发者在本地进行更改,配置主题设置,创建新页面,添加高级自定义字段,调整插件选项等。与此同时,实时版本正在不断变化。可能是客户在写新的博客文章,或者是电子商务网站添加新产品,或者客户下订单。当开发者准备将本地更改推送到实时站点时,他们的更改有可能覆盖实时站点上的任何更改。这是一个影响所有数据库驱动的内容管理系统的问题,也是WordPress网站开发中最棘手的部分之一。目前,没有简单的方法来协调这些更改并进行简单的合并。

哎……我讨厌在开发环境和生产环境之间合并数据库更改。#wordpress #dba #devlife

— Shawn Hooper (he/him) (@ShawnHooper) 2015年1月4日

当变通方法不起作用时

对于简单的宣传册或营销网站,不需要数据合并和冲突解决。在任何开发周期期间,实时站点上的数据基本保持不变。复制任何新的媒体文件,并使用诸如WP Migrate之类的插件_partial_推送MySQL数据库中有更改的表,就足够了。对于更复杂的网站,WordPress使用的数据架构(并鼓励开发者使用)使得执行选择性数据迁移变得极其困难。

例如,在电子商务商店中,实时站点上唯一变化的数据是商店所有者的产品和客户的订单,理论上部分数据迁移应该可以工作。理想情况下,存储这些数据的数据表在推送网站开发期间所做的其他数据更改时可以忽略。WordPress只有一个问题——自定义文章类型。

cpt meme

为什么自定义文章类型使问题复杂化

WordPress有一个极其灵活的数据结构,允许不同类型的数据对象存储在wp_posts表中。不同的数据对象被称为自定义文章类型,其类型由wp_posts表中的post_type列定义。对于存储不仅是文章,还包括页面、菜单、附件、修订版本、菜单项以及任何注册为自定义文章类型的数据对象的记录的表来说,这不是最直观的名称。可以考虑房地产网站的“house”、动物救助网站的“animal”,或者产品网站文档部分的简单“doc”。

当自定义文章类型在 WordPress 3.0 中推出时,人们对于这个能让 WordPress 安装不仅限于博客的功能欢呼雀跃。然而,我认为自定义文章类型及其数据模型实现已经对 WordPress 生态系统造成了负面影响。

一方面,这是一个非常简洁灵活的系统,有一个简单的 PHP API 只需几行代码就能注册新的数据对象。注册自定义文章类型后,你可以免费获得很多功能,而无需自己编写代码;一个新的菜单选项、简单的CRUD(创建、读取、更新、删除)管理、一个用于列表和批量操作的表格界面,甚至还有 REST API 端点。

另一方面,注册新类型的便捷性意味着有时错误的数据对象类型也被以这种方式存储。就像我之前提到的电商产品和订单的例子——对店铺所有者最重要的数据与所有其他文章类型保存在同一张表中。实时站点会随着新订单的创建不断向 wp_posts 添加新记录。如果对本地站点的文章或页面做了任何更改,那么 wp_posts 表就无法再推送到线上,否则就会丢失那些订单!

自定义文章类型还可能为 WordPress 站点带来其他问题。用它们来存储各种类型的数据会导致性能问题。WordPress 在前端显示文章、页面和其他文章类型时会持续查询 wp_posts 表,而随着该表不断增长,这些查询也会变得越来越慢。当然缓存可以提供帮助,但应该更仔细地考虑将可能大幅增长的数据存储在哪里。

值得庆幸的是,插件开发者意识到了这些问题,并开始改变他们的做法。

WooCommerce 在 2022 年初宣布了自定义订单表的官方计划,后来被称为高性能订单存储(HPOS)。从 WooCommerce 8.2 开始,它在新安装中默认启用。

Easy Digital Downloads 在 EDD 3.0.0 版本中完全将其所有自定义文章类型迁移到了自定义表,取代了旧的注册 'edd_log' 文章类型来记录每次文件下载的系统。在旧系统下,日志记录可能会扩展到大量的行,而这些在 wp_posts 表中是不必要的。

这些改进有所帮助,但永远无法完全解决数据库合并的问题。在数据库迁移中忽略某些表只能帮你到这一步。大多数情况下,实时站点的变化方式使得对开发站点做的任何更改都无法自动合并回来,而这正是手动方法可能仍然是最好的地方。

你有什么替代方案?

这就是我们在开发现已停用的 Mergebot 时意识到的。对于每个写入数据库的插件或主题,你需要知道它如何存储数据,这样才能正确处理 ID 和冲突。试图解决所有这些冲突最终会变得非常复杂。即使有一个支持所有主流插件和主题的解决方案,开发者仍然需要手动配置不支持的插件/主题,而这往往比数据库部署本身更麻烦。如果你只需要做一次,那还不算太糟,但每次更新插件或主题时,它都可能会改变存储数据的方式。这是一个永无止境的巨大麻烦。

有几种方法可以缓解这些冲突。

SQL 脚本

一种解决方案是在本地开发环境中将任何数据更改记录为 SQL 脚本。让我们看一个简单的例子。安装 WooCommerce 需要您创建一些页面,例如结账页面和我的账户页面。一旦这些页面被创建,您可以从数据库导出数据来创建一个包含用于创建这些页面的查询的 SQL 文件:

INSERT INTO `wp_posts` (`ID`, `post_author`, `post_date`, `post_date_gmt`, `post_content`, `post_title`, `post_excerpt`, `post_status`, `comment_status`, `ping_status`, `post_password`, `post_name`, `to_ping`, `pinged`, `post_modified`, `post_modified_gmt`, `post_content_filtered`, `post_parent`, `guid`, `menu_order`, `post_type`, `post_mime_type`, `comment_count`) VALUES
(NULL, 1, '2013-02-27 21:03:13', '2013-02-28 01:03:13', '[woocommerce_checkout]', 'Checkout', '', 'publish', 'closed', 'closed', '', 'checkout', '', '', '2013-11-04 09:52:16', '2013-11-04 13:52:16', '', 0, 'http://yourwebsite.com/checkout/', 0, 'page', '', 0),
(NULL, 1, '2013-02-27 21:03:13', '2013-02-28 01:03:13', '[woocommerce_my_account]', 'My Account', '', 'publish', 'closed', 'closed', '', 'my-account', '', '', '2013-11-04 09:52:50', '2013-11-04 13:52:50', '', 0, 'http://yourwebsite.com/my-account/', 0, 'page', '', 0);

一旦准备好部署您的更改,将 SQL 脚本导入生产数据库,就可以了。

PHP 脚本

您可以编写一个创建数据的 PHP 脚本,而不是事后记录 SQL 查询。当然,您也可以在其中包含 SQL 查询,但您还可以做更多的事情。让我们再次使用上面的场景作为示例,但这次我将编写一个函数来部署更改。

/*
 * To run the deployment
 * 1. Log in as an administrator
 * 2. Go to http://hellfish.media/?deploy_changes=1
 */
add_action( 'template_redirect', 'deploy_changes' );
function deploy_changes() {

  // If the request is not our deployment
  if ( !isset( $_GET['deploy_changes'] ) ) {
    return;
  }

  // Only allow a logged in admin to run the deployment
  if ( !current_user_can( 'install_themes' ) ) {
    echo 'Not logged in.';
    exit;
  }

  // Run script for 5 mins max
  set_time_limit(60*5);

  $pages = array(
    array(
    'content' => '[woocommerce_checkout]',
    'title'   => 'Checkout'
    ),
    array(
    'content' => '[woocommerce_my_account]',
    'title'   => 'My Account'
    )
  );

  foreach ( $pages as $page ) {
    $new_page = array(
    'post_type'   => 'page',
    'post_title'  => $page['title'],
    'post_content'  => $page['content'],
    );

    wp_insert_post( $new_page );
  }

  echo 'Deployment complete.';
  exit;
}

您可以将此代码放在自定义的 maintenance.php 文件中,并从主题的 functions.php 文件中包含它,或者将其作为自定义 WordPress 插件添加,而不是包含在主题中,但两种方式都可以。一旦准备好部署,将此代码推送到您的实时站点并以管理员身份登录。然后通过在域名末尾附加 deploy_changes 查询字符串来运行代码,例如:

http://hellfish.media/?deploy_changes=1

完成更改后,从文件中删除该函数(或禁用该插件)以确保您不会意外重新运行部署。版本控制(如 Git)可确保您始终保留旧部署代码的历史记录,以防您再次需要它。

您可以进一步简化这一步骤。在自定义插件中,您可以在数据库中存储一个版本号,并根据版本号仅运行特定的更改。这样,您只需添加基于版本号运行的新代码,并在每部分代码运行后更新版本号。让我们扩展上面的示例,使用这种版本检查方法来包含一个新页面。

/*
 * 运行部署
 * 1. 以管理员身份登录
 * 2. 访问 http://hellfish.media/?deploy_changes=1
 */
add_action( 'template_redirect', 'deploy_changes' );
function deploy_changes() {

    // 如果请求不是我们的部署
    if ( ! isset( $_GET['deploy_changes'] ) ) {
        return;
    }

    // 只允许已登录的管理员运行部署
    if ( ! current_user_can( 'install_themes' ) ) {
        echo '未登录。';
        exit;
    }

    // 运行脚本最多5分钟
    set_time_limit( 60 * 5 );

    // 从wp_options表获取版本号,默认为0
    $version = get_option( 'hellfish_custom_plugin_version', '0' );

    // 仅在$version为0时运行
    if ( version_compare( $version, '0', '=' ) ) {
        $pages = array(
            array(
                'content' => '[woocommerce_checkout]',
                'title'   => 'Checkout'
            ),
            array(
                'content' => '[woocommerce_my_account]',
                'title'   => 'My Account'
            )
        );

        deploy_pages( $pages );

        // 更新wp_options表中存储的版本
        update_option( 'hellfish_custom_plugin_version', '1' );
        echo '部署完成。';
        exit;
    }

    // 仅在$version为1时运行
    if ( version_compare( get_bloginfo( 'version' ), '1', '=' ) ) {
        $pages = array(
            array(
                'content' => '[woocommerce_cart]',
                'title'   => 'Cart'
            )
        );

        deploy_pages( $pages );
        update_option( 'hellfish_custom_plugin_version', '2' );
        echo '部署完成。';
    }
}

// 部署在deploy_changes函数中设置的任何页面
function deploy_pages( $pages ) {
    foreach ( $pages as $page ) {
        $new_page = array(
            'post_type' => 'page',
            'post_title'   => $page['title'],
            'post_content' => $page['content'],
        );

        wp_insert_post( $new_page );
    }
}

将其作为自定义插件的一部分构建意味着您可以将可能需要的任何数据库更新与每次插件发布时更新的插件版本号关联起来。

我们做什么

我们在 Delicious Brains 和 ACF 站点 使用了类似的方法,但我添加了更多的控制和流程。受到 Laravel 的数据库迁移数据填充系统的启发,我创建了一个迁移库,帮助编写脚本以便在开发过程中进行表和数据更改。与现有的 wp-table-migrations 包有相当多的重叠,但我想要一个可以在不同站点上使用的自定义解决方案。

流程如下:

  1. 每个迁移文件一旦运行,将被记录在 migrations 表中以确保它不会再次运行
  2. 该文件可以作为功能分支的一部分提交到 Git
  3. 实时站点的部署流程在文件复制到服务器后有一个构建步骤,通过自定义的 wp dbi migrate WP-CLI 命令运行数据库迁移

这样就可以编写 PHP 代码来更改表并修改数据,作为新功能或错误修复的任何代码更改的一部分。这些更改存储在 Git 中,并构成 GitHub 拉取请求的一部分,经过审查、合并并最终部署。使用此流程意味着您需要了解 WordPress 代码如何更改数据库,这样您就可以使用如下代码进行模式更改:

<?php

namespace DeliciousBrainsWPMigrationsDatabaseMigrations;

use DeliciousBrainsWPMigrationsDatabaseAbstractMigration;

class ComposerStatsTable extends AbstractMigration {

    public function run() {
        global $wpdb;

        $sql = "
            CREATE TABLE " . $wpdb->prefix . "woocommerce_software_composer_download_statistics (
                id bigint(20) NOT NULL auto_increment,
                composer_key varchar(50) NOT NULL,
                package varchar(50) NOT NULL,
                version varchar(16) NOT NULL,
                original_package varchar(50) NOT NULL,
                download_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
                PRIMARY KEY  (id)
            ) {$this->get_collation()};
        ";

        dbDelta( $sql );
    }
}

以及像这样对数据进行更改:

<?php

namespace DeliciousBrainsWPMigrationsDatabaseMigrations;

use DeliciousBrainsWPMigrationsDatabaseAbstractMigration;

class AddWpSesLandingPage extends AbstractMigration {

    public function run() {
        $page_id = wp_insert_post( array(
            'post_title'  => 'WP SES is Now WP Offload SES Lite',
            'post_status' => 'publish',
            'post_name'   => 'wp-ses-now-wp-offload-ses-lite',
            'post_type'   => 'page',
            'post_parent' => 49760,
        ) );

        update_post_meta( $page_id, '_wp_page_template', 'page-wp-ses-landing.php' );
        update_post_meta( $page_id, '_yoast_wpseo_metadesc', 'Looking for WP-SES.com? WP SES is now WP Offload SES Lite. See what improvements we've made to the free version and what you can get when you upgrade.' );
    }
}

我使用 WordPress PHP 代码手动编写数据库更改脚本,可以使用命令行上的 wp dbi migrate 进行测试。如果在迁移类的 rollback 方法中包含回滚代码,我还可以回滚最后一个迁移。通过这样的脚本化方式控制对数据库的更改,但有时我确实希望能使用 WordPress 管理后台来进行更改并执行合并,就像我们之前希望 Mergebot 能够实现的那样。

市场上的其他合并解决方案呢?

荣誉提及:VersionPress

当我们开始开发 Mergebot 时,有一个类似的插件使用 Git 来跟踪 WordPress 文件和数据库的更改,叫做 VersionPress

不幸的是,2020 年 6 月,开发团队宣布 VersionPress 的开发已经结束

WPMerge

在我们决定关闭 Mergebot 不久后,一个新的合并解决方案进入了测试阶段。WPMerge 在很多方面与 Mergebot 的工作方式类似。你确保开发数据库与线上同步,然后开始记录,之后对开发站点进行更改,以便稍后应用到线上。

发布后不久,WPMerge 与 Nexcess 合作,为其 WordPress 和 WooCommerce 托管客户增加更多价值。

我对 WPMerge 进行了快速测试,效果很好。我确实注意到它与 Mergebot 在实现方式上存在一些差异。它在开发站点的数据库上使用数据库触发器来记录所有执行的查询,而不是使用 WordPress 的 'query' 过滤器来记录。

在 brief 测试中,我做了以下操作:

  • 在开发环境中添加一篇新文章,确保部署到线上站点时它与已部署的文章 ID 具有正确的文章元关系
  • 添加一篇包含新上传图片图库的新文章,以查看添加到文章内容中的图库短代码中的媒体文章 ID 在部署时是否被正确替换——它们被正确替换了 🎉
  • 在开发站点上编辑现有文章的标题并部署到线上,而线上的标题也已更改——开发环境的编辑被直接使用了,没有询问 😬

不幸的是,WPMerge 同样无法处理冲突解决:

目前,就像 Git 一样,开发环境的更改优先。我们正在开发一个冲突解决模块,让您可以选择保留哪些更改。

即使作为竞争对手的前开发者,WPMerge 也给我留下了深刻印象,它开箱即用。我确实认为 UI/UX 可以做一些改进,让开发者更容易使用,并更好地理解工作流程。我发现自己对应该采取什么正确操作才能将当前的开发更改应用到正式环境感到非常困惑。我还注意到被记录和部署的查询数量太多了,其实对于我正在做的更改来说并不是必需的。在我上面描述的简单一轮测试之后,该插件记录了超过 400 条查询!这让我对在安装了大量插件的大型网站上使用它感到紧张。

虽然 WPMerge 仍在积极开发中,但除了核心功能外,似乎没有添加任何新功能。最新 的更新日志 只反映了一些错误修复和 PHP 8 支持的更新。

总结

我仍然不相信 WordPress 数据库合并有什么灵丹妙药。这是一个开发者需要意识到的问题,这样他们就不会陷入本地数据库和正式数据库不同步的困境。在做出更改之前要做好准备,记录您所做的操作,然后在部署时回放这些更改。对我来说,让这个过程尽可能系统化和自动化是改进开发工作流程的关键。它还可以减少人为错误,尤其是在团队环境中,因为每个数据更改都会被提交到版本控制中。

您将开发数据库更改应用到正式网站的首选方法是什么?您是否找到了满意的数据库合并工具?请在评论中告诉我们。

分享你的喜爱

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注