tpetry/laravel-postgresql-enhanced

支持许多缺失的 PostgreSQL 特定功能

1.1.1 2024-09-23 15:49 UTC

README

License PHP Latest Version on Packagist Downloads on Packagist GitHub PHPUnit Action Status GitHub PHPStan Action Status GitHub PhpCsFixer Action Status

Laravel 支持许多不同的数据库,因此不得不限制自己到所有数据库的最低共同点。然而,PostgreSQL 提供了更多功能,这些功能通过此扩展添加到 Laravel 中。

安装

您可以通过 composer 安装此包

composer require tpetry/laravel-postgresql-enhanced

版本策略

此包遵循语义版本控制策略。 因此,您可以始终安全地升级到新的次要和补丁版本,而不会出现任何问题。

由于 Laravel 是一个健康且仍在添加新数据库功能的框架,因此某些事情可能会出错:例如,过去 Laravel 引入了新的构建器方法,这些方法已经在此包中存在很长时间了 - 但语义不同。在这种情况下,会立即发布一个新的大版本,并进行破坏性更改以与 Laravel 的实现保持一致。

当升级 Laravel 时,如果出现问题,您可能还需要升级此包到新的大版本!过去几年的最小破坏性更改列在 破坏性更改部分 中。

功能

IDE 自动完成

Laravel 提供了许多扩展功能,这使得 IDE 很难进行适当的自动完成。目前,没有通用方法可以告诉 IDE 关于此增强驱动程序添加的功能。但是,此包已与特定 IDE 组合集成

  • PhpStorm with Laravel Idea: 可以通过从 Laravel -> Code Generation 工具栏菜单中运行 Generate Helper Code (Eloquent, Maco, Facades, etc.) 动作来检测所有 PostgreSQL 功能,使用 IDE Helper for Laravel

PHPStan

此扩展向 Laravel 添加了许多缺失的 PostgreSQL 功能。已开发一组自定义的 PHPStan 扩展,以获得完整的静态分析支持!

本扩展提供的所有功能都自动由 phpstan/extension-installer 识别。否则,您需要手动将以下路径添加到 phpstan.neon 中的 includes 列表中,您的配置现在应该如下所示

includes:
    - ./vendor/nunomaduro/larastan/extension.neon
    - ./vendor/tpetry/laravel-postgresql-enhanced/phpstan-extension.neon

迁移

零停机迁移

对于24/7运行的应用程序,迁移绝不能影响可用性。PostgreSQL提供了许多功能,可以在不停机的情况下执行模式更改。然而,有时对模式的更改测试不足,并且为了实现预期的更改,表会被锁定更长的时间。为了避免这个问题,可以将迁移标记为零停机迁移。如果迁移超过了指定的超时时间,它将被取消,并将模式重置到原始状态。

use Illuminate\Database\Migrations\Migration;
use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Schema\Concerns\ZeroDowntimeMigration;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

class Test123 extends Migration
{
    use ZeroDowntimeMigration;

    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::table('user', function (Blueprint $table) {
            $table->string('name', 128)->change();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('user', function (Blueprint $table) {
            $table->string('name', 32)->change();
        });
    }
}

可以为每个迁移单独设置最大超时时间的超时时间。您可以在迁移上设置 private float $timeout = 5.0 以设置上/下超时。或者,您可以将特定的超时时间 $timeoutUp$timeoutDown 设置来区分方法。

扩展

创建扩展

Schema 门面支持使用 createExtensioncreateExtensionIfNotExists 方法创建扩展

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::createExtension('tablefunc');
Schema::createExtensionIfNotExists('tablefunc');

删除扩展

要删除扩展,您可以使用 Schema 门面提供的 dropExtensiondropExtensionIfExists 方法

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::dropExtension('tablefunc');
Schema::dropExtensionIfExists('tablefunc');

您可以通过传递多个扩展名称一次性删除多个扩展

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::dropExtension('tablefunc', 'fuzzystrmatch');
Schema::dropExtensionIfExists('tablefunc', 'fuzzystrmatch');

函数

创建函数

Schema 门面支持使用 createFunctioncreateFunctionOrReplace 方法创建函数。对于您函数的定义,您必须提供函数的名称、参数、返回类型、函数的语言和主体

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::createFunction(
  name: 'sales_tax',
  parameters: ['subtotal' => 'numeric'],
  return: 'numeric',
  language: 'plpgsql',
  body: '
    BEGIN
      RETURN subtotal * 0.06;
    END;
  '
);

第六个参数允许您为函数定义进一步选项。请阅读手册以获取确切含义,其中一些设置启用或禁用PostgreSQL优化执行的方式。

前面的示例可以通过使用此驱动程序创建的特殊 sql:expression 语言标识符进行优化。函数主体只能是一个SQL表达式,但它将在查询中内联,而不是在最新的PostgreSQL版本中执行,从而获得更好的性能

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::createFunction('sales_tax', ['subtotal' => 'numeric'], 'numeric', 'sql:expression', 'subtotal * 0.06', [
  'parallel' => 'safe',
  'volatility' => 'immutable',
]);

如果您的函数要返回一个表,您必须提供列作为返回类型

Schema::createFunction('search_user', ['pattern' => 'text'], ['id' => 'int', 'email' => 'text'], 'plpgsql', "
  BEGIN
    RETURN QUERY select user_id, contactemail from users where name ilike '%' || pattern || '%';
  END;
");

删除函数

要删除函数,您可以使用 Schema 门面提供的 dropFunctiondropFunctionIfExists 方法

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::dropFunction('sales_tax');
Schema::dropFunctionIfExists('sales_tax');

触发器

创建触发器

您可以在 Blueprint 上为表添加触发器。您需要传递一个唯一名称、之前创建的函数的调用以及将触发触发器的动作

use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('projects', function (Blueprint $table): void {
    $table->trigger('rollup_quota', 'update_quota_by_projects()', 'AFTER INSERT OR DELETE');
});

以下表格包含所有可用的触发器修饰符

注意

PostgreSQL即使在没有任何更改的情况下也会更新行,这可能会影响您的性能。您可以在所有表中添加具有 BEFORE UPDATE 动作的 suppress_redundant_updates_trigger() 触发器。

删除触发器

要删除触发器,您可以使用表 Blueprint 类提供的 dropTriggerdropTriggerIfExists 方法

use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('projects', function (Blueprint $table): void {
    $table->dropTrigger('update_quota');
    $table->dropTriggerIfExists('update_quota');
});

视图

创建视图

Schema 门面支持使用 createViewcreateViewOrReplace 方法创建视图。您视图的定义可以是一个SQL查询字符串或查询构建器实例

use Illuminate\Support\Facades\DB;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::createView('users_with_2fa', 'SELECT * FROM users WHERE two_factor_secret IS NOT NULL');
Schema::createViewOrReplace('users_without_2fa', DB::table('users')->whereNull('two_factor_secret'));

您可以通过传递一个数组作为最后一个参数来指定视图列的替代名称

use Illuminate\Support\Facades\DB;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::createView('users_with_2fa', DB::table('users')->select('id')->whereNull('two_factor_secret'), ['user_id']);

如果您需要创建递归视图,可以使用 createRecursiveViewcreateRecursiveViewOrReplace 方法,就像之前的示例一样,但您需要提供最后参数为可用列。

use Illuminate\Support\Facades\DB;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

// TODO simple example explaining the concept
Schema::createRecursiveView('viewname', 'SELECT id, col1, col2 FROM ....', ['id', 'col1', 'col2']);
Schema::createRecursiveViewOrReplace('viewname', 'SELECT id, col1, col2 FROM ....', ['id', 'col1', 'col2']);

删除视图

要删除视图,您可以使用由 Schema 门面提供的 dropViewdropViewIfExists 方法。您无需区分常规视图和递归视图。

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::dropView('myview');
Schema::dropViewIfExists('myview');

您可以通过传递多个视图名称一次性删除多个视图。

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::dropView('myview1', 'myview2');
Schema::dropViewIfExists('myview1', 'myview2');

物化视图

对于物化视图,您可以在查询执行时填充视图的内容。您可以使用它们来缓存昂贵的查询,这样就不必每次都重新运行。

物化视图的创建(和删除)与常规视图相同。您可以选择传入查询构建器或原始 SQL 查询。对于非常慢的查询,创建不带初始数据的物化视图是一个有用的方法。通过传递 withData: false 参数,物化视图立即创建,不会存储任何数据,您需要稍后刷新它以包含一些数据。

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::createMaterializedView('users_with_2fa', 'SELECT * FROM users WHERE two_factor_secret IS NOT NULL');
Schema::createMaterializedView('users_with_2fa', DB::table('users')->whereNull('two_factor_secret'));
Schema::createMaterializedView('users_with_2fa', DB::table('users')->select('id')->whereNull('two_factor_secret'), columns: ['user_id']);

Schema::createMaterializedView('very_slow_query_materialized', 'SELECT ...', withData: false);

Schema::dropMaterializedView('users_with_2fa');
Schema::dropMaterializedViewIfExists('users_with_2fa');

您可以在任何时候刷新创建的物化视图的存储值。当传递 concurrently: true 参数时,命令将立即完成,PostgreSQL 将在后台刷新值。您还可以使用 withData: truewithData: false 参数更改物化视图的行为,使其不再包含任何数据。

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::refreshMaterializedView('users_with_2fa');
Schema::refreshMaterializedView('users_with_2fa', concurrently: true);
Schema::refreshMaterializedView('users_with_2fa', withData: false);
Schema::refreshMaterializedView('users_with_2fa', withData: true);

索引

唯一索引

Laravel 通过 $table->unique() 方法提供唯一性,但这些是唯一约束而不是唯一索引。如果您想使表中的值唯一,它们的行为将相同。然而,只有对于唯一索引,才提供高级选项,如部分索引、包括其他列或列选项。

为了使用这些功能而不破坏与 Laravel 的兼容性,已添加了 uniqueIndex 方法,可以像使用 unique 一样使用。

use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('users', function(Blueprint $table) {
    $table->uniqueIndex('email');
});

如果存在则删除

除了 Laravel 的删除索引方法外,还添加了删除索引(如果存在)的方法。方法 dropFullTextIfExistsdropIndexIfExistsdropPrimaryIfExistsdropSpatialIndexIfExistsdropSpatialIndexIfExists 与其 Laravel 原本的语义匹配。

并发

与 PostgreSQL 一起,您可以告别因错误而半执行的迁移以及将数据库恢复到稳定状态的繁琐工作。这一切都归功于其事务方法:迁移对数据库的所有更改要么成功,要么回滚。太好了!因此,在大型表上创建索引将花费很长时间,并且在该期间将阻止所有 SQL 查询。现在,您可以指示 PostgreSQL 在后台创建索引而不阻止任何 SQL 查询,但您必须选择不运行这些更改的事务。

<?php

use Illuminate\Database\Migrations\Migration;
use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

return new class extends Migration
{
    public $withinTransaction = false;

    public function up(): void
    {
        Schema::table('blog_visits', function (Blueprint $table) {
            $table->index(['url', 'ip_address'])->concurrently();
        });
    }
};

空值非唯一

对于大多数开发人员来说,唯一索引中的 NULL 值的处理方式难以理解。例如,当您保存活动订阅时,您希望通过例如在 (user_id, cancelled_at) 上创建唯一索引来限制每个用户只有一个活动订阅。但是,由于活动订阅没有 cancelled_at 时间戳,因此可以输入具有相同 user_idcancelled_at 的 NULL 值的多行,因为两个 NULL 值永远不会相同。但是,从 PostgreSQL 15 开始,您现在可以指示数据库不允许这些重复的行,通过将 NULL 值视为非不同来处理。

use Illuminate\Database\Query\Builder;
use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::create('subscriptions', function(Blueprint $table) {
    $table->id('user_id');
    $table->timestampTz('cancelled_at');


    $table->uniqueIndex(['user_id', 'cancelled_at'])->nullsNotDistinct();
});

注意

对于此示例,您还可以使用 user_id 的唯一部分索引,并限制行为为 cancelled_at IS NOT NULL

部分索引

部分索引是在表的一个子集上建立的索引;子集由一个条件定义。索引只包含满足条件的表行条目。部分索引是一个特殊功能,但在某些情况下非常有用。例如,如果你想使用户表的电子邮件地址列是唯一的,而你正在使用软删除。这是不可能的,因为删除一个用户并再次创建它,电子邮件地址会被使用两次。使用部分索引可以通过限制索引只包含未删除的行来实现。

use Illuminate\Database\Query\Builder;
use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('users', function(Blueprint $table) {
    $table->uniqueIndex('email')->where("deleted_at IS NULL");
    // or:
    $table->uniqueIndex('email')->where(fn (Builder $condition) => $condition->whereNull('deleted_at'));
});

部分索引是通过在由 fullText()index()spatialIndex()uniqueIndex() 创建的索引上使用 where 方法来创建的。

包含列

最近几个 PostgreSQL 版本的一个真正出色的功能是能够将列包括在索引中作为非键列。非键列不用于高效查找,但 PostgreSQL 可以使用这些列来执行索引唯一操作,无需从表中加载特定的列,因为它们已经包含在索引中。

use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('users', function(Blueprint $table) {
    // The query "SELECT firstname, lastname FROM users WHERE email = 'test@example.com'" can be executed as an index-only scan without loading the table data
    $table->index('email')->include(['firstname', 'lastname']);
});

列是通过在由 index()spatialIndexuniqueIndex 创建的索引上使用 include 方法来包含在索引中的。

如果不存在

有时,你通过在生产数据库上测试新索引来解决性能问题,而不是每次都推送新的迁移。但是当你找到完美的索引时,你也应该为它创建一个迁移。删除现有索引以通过迁移重新创建它是愚蠢的。对吧?现在,当确切的索引已经存在时,你可以从迁移中省略索引创建。

use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('invoices', function(Blueprint $table) {
    $table->index(['target', 'division', 'date'])->ifNotExists();
});

技巧

索引通过它们的(自动生成的)名称来确定是否相同。要么通过从你的开发机器上的临时迁移来运行生产环境中的索引语句,要么使用特定的索引名称。

存储参数(索引)

在某些情况下,你需要指定索引的存储参数。如果你正在使用 gin 索引,你应该阅读文章《调试 PostgreSQL 中的随机慢速写入》,了解为什么 gin 索引的存储参数很重要。

use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('bookmarks', function(Blueprint $table) {
    $table->index('data')->algorithm('gin')->with(['fastupdate' => false]);
});

存储参数是通过在由 fullText()index()spatialIndexuniqueIndex 创建的索引上使用 with 方法来定义的。

函数索引/列选项

有时,仅包含列指定的索引不足以满足需求。在某些情况下,为了获得最佳性能,必须使用 PostgreSQL 的扩展索引功能。

  • 要创建函数索引,函数必须括在括号中,并且必须指定一个单独的索引名称,因为无法从表达式中自动生成索引名称。
  • 列特定属性,如排序、排序或 NULL 值的位置,可以像在正常的 SQL 查询中一样,直接在列名称后轻松指定。
use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('users', function(Blueprint $table) {
    $table->uniqueIndex('(LOWER(email))', 'users_email_unique');
    $table->index(['firstname ASC NULLS FIRST', 'lastname ASC NULLS FIRST']);
    $table->index('attributes jsonb_path_ops')->algorithm('gin');
});

全文索引

PostgreSQL 的全文搜索依赖于语言:为了获得更好的结果,所有单词都被 词干提取 到它们的根形式。Laravel 默认使用 english 语言,对于你的应用程序,你可能需要使用不同的语言。你也可以使用通用的 simple 语言,它不会进行词干提取,但你的搜索词将必须包含与记录匹配的精确字符串。

此外,你可以为索引的每一列指定一个相对权重,以控制排名。在这个例子中,title 列比 description 列更相关,因此其相对权重被设置为更重要(AB 之前)。

有关全文搜索的所有选项的更多信息,请参阅这篇文章:使用 PostgreSQL 12 调整全文搜索

use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('book', function (Blueprint $table) {
    $table->fullText(['title', 'description'])
        ->language('spanish')
        ->weight(['A', 'B']);
});

域类型

类似于PostgreSQL这样的关系型数据库提供了许多可以选择的数据类型。但它们只是通用的类型,应该匹配成千上万个应用程序。您的特定需求并未涵盖。但是,通过领域类型,您可以添加特定于应用程序的类型,例如一个具有特定数字数量且永远不会为负的价格列:将现有的基本类型(numeric(9,2))重命名为一个新的类型,该类型具有一个可选条件,即所有值都必须匹配。您可以使用它来创建可重复的价格列或创建全新的类型,例如必须匹配特定格式的车牌类型。

创建领域类型

Schema外层支持通过传递名称、基本类型和可选的SQL条件来验证任何值,使用createDomain方法创建领域类型。

use Tpetry\PostgresqlEnhanced\Query\Builder;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::createDomain('price', 'numeric(9,2)');
Schema::createDomain('price', 'numeric(9,2)', 'VALUE >= 0');
Schema::createDomain('price', 'numeric(9,2)', fn (Builder $query) => $query->where('VALUE', '>=', 0));

使用领域类型

您创建的领域类型可以使用在迁移中,就像其他任何列一样,使用domain列类型,并使用列名称和领域类型名称。

Schema::create('products', function (Blueprint $table): void {
  $table->id();
  $table->string('item_name');
  $table->domain('item_price', 'price');
  $table->timestampsTz();
});

注意

您还可以利用领域类型来使用例如通过扩展添加的列类型或尚未由包支持的列类型。

修改领域类型

领域类型的基本类型在创建后不能更改。但您可以更改验证值的条件,通过删除它或用一个新的替换它。

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

// To drop the validation condition:
Schema::changeDomainConstraint('price', null);

// To change validation condition:
Schema::changeDomainConstraint('price', 'VALUE > 0');
Schema::changeDomainConstraint('price', fn (Builder $query) => $query->where('VALUE', '>', 0));

删除领域类型

要删除领域类型,首先必须删除所有使用它们的列(或更改它们的类型),然后使用Schema外层提供的dropDomaindropDomainIfExists

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::dropDomain('price');
Schema::dropDomainIfExists('price');

您可以通过传递多个领域名称一次删除多个领域类型。

use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::dropDomain('price', 'license_plate');
Schema::dropDomainIfExists('price', 'license_plate');

表选项

未记录

如果丢失数据不是问题,并且您希望在写入操作中获得很大的速度提升,则可以将高写入负载表标记为未记录。未记录表在PostgreSQL中写入磁盘,但跳过了一些为了崩溃安全而必须满足的持久性要求。它们的行为与任何其他表一样,并在干净关闭时保持数据,而在服务器崩溃时所有数据都会丢失。这是一个完美的选项,例如对于您愿意丢失的临时数据,例如会话,因为每个用户都可以再次登录。您可以使用表蓝图上的unlogged方法激活和停用未记录表模式。

use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('sessions', function (Blueprint $table): void {
    // make the table unlogged
    $table->unlogged();
    
    // make the table crash-safe again
    $table->unlogged(false);
});

存储参数(表)

使用存储参数,您可以根据应用程序的要求和特定的负载调整表。您可能想要更改的存储参数和选项:

您可以在调整autovacuum指南中找到针对特定工作负载的更多建议。

use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('sessions', function (Blueprint $table): void {
    $table->with([
        // Tune statistics generation for tables with millions of records
        'autovacuum_analyze_scale_factor' => 0.02,
        // Tune table for frequent UPDATE statements
        'fillfactor' => 90,
    ]);
});

列选项

压缩

PostgreSQL 14引入了为可压缩数据类型指定压缩方法的可能性。您可以选择默认方法pglz、最近添加的lz4算法或使用default值来使用服务器的默认设置。

use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('books', function (Blueprint $table): void {
    // @see https://postgresql.ac.cn/docs/current/storage-toast.html
    $table->string('summary')->compression('lz4');
});

初始

有时需要添加一个新列,并且所有现有行都应该有一个初始值。使用initial修饰符,您可以为所有现有行分配一个值,而所有新行将没有默认值或不同的值。

use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::table('users', function (Blueprint $table): void {
    $table->boolean('acl_admin')->initial(false);
    $table->boolean('acl_read')->initial(false)->default(true);
});

使用

PostgreSQL禁止在迁移中更改一些数据类型,如果它们违反类型系统。例如,您不能将存储一个电子邮件地址的varchar列更改为存储多个电子邮件地址的jsonb数组,因为PostgreSQL不知道如何自动在这些类型之间进行转换。您会得到这个错误

SQLSTATE[42804]: Datatype mismatch: 7 ERROR:  column "email" cannot be cast automatically to type jsonb

您可以使用 using() 修饰符指定一个表达式,以将当前值转换为新的类型。

Schema::table('users', function (Blueprint $table): void {
    $table->jsonb('email')->using('jsonb_build_array(email)')->change();
});

列类型

数组

数组数据类型可以在一个单独的列中存储多个值。例如,可以用来存储产品所属的分类的多个标签ID。

// @see https://postgresql.ac.cn/docs/current/arrays.html
$table->integerArray(string $column);

注意

尽管 PostgreSQL 的数组类型功能强大,但只支持整数数组。与 JSON 列相比,它是唯一具有 PostgreSQL 操作和查询增强功能的数组类型。intarray 扩展提供了广泛的功能,例如,可以用来存储和查询带有高级布尔逻辑的标签

范围

范围数据类型可以存储具有可选起始和结束值的值范围。例如,可以用来描述会议室预订的持续时间。

// @see https://postgresql.ac.cn/docs/current/rangetypes.html
$table->bigIntegerRange(string $column);
$table->bigIntegerMultiRange(string $column);
$table->dateRange(string $column);
$table->dateMultiRange(string $column);
$table->decimalRange(string $column);
$table->decimalMultiRange(string $column);
$table->integerRange(string $column);
$table->integerMultiRange(string $column);
$table->timestampRange(string $column);
$table->timestampMultiRange(string $column);
$table->timestampTzRange(string $column);
$table->timestampTzMultiRange(string $column);

位字符串

位字符串数据类型可以存储由 0 和 1 组成的字符串。例如,可以用来存储位图。

// @see https://postgresql.ac.cn/docs/current/datatype-bit.html
$table->bit(string $column, int $length = 1);
$table->varbit(string $column, ?int $length = null);

不区分大小写的文本

不区分大小写的文本类型用于存储将被不区分大小写比较的文本。例如,可以用来存储和比较电子邮件地址。

// @see https://postgresql.ac.cn/docs/current/citext.html
$table->caseInsensitiveText(string $column);

注意

在之前,您需要使用 Schema::createExtension('citext')Schema::createExtensionIfNotExists('citext') 启用 citext 扩展。

全文搜索

tsvector 类型用于存储用于全文搜索的处理过的词典。

// @see https://postgresql.ac.cn/docs/10/datatype-textsearch.html
$table->tsvector(string $column);

IP 网络

ip 网络数据类型以 cidr 表示法存储 ip 网络。

// @see https://postgresql.ac.cn/docs/current/datatype-net-types.html
$table->ipNetwork(string $column);

Hstore

hstore 数据类型用于在单个 PostgreSQL 值中存储键/值对。新的 json 数据类型在所有方面都更好,因此 hstore 应仅用于与旧应用程序的兼容性。

// @see https://postgresql.ac.cn/docs/current/hstore.html
$table->hstore(string $column);

注意

在之前,您需要使用 Schema::createExtensionIfNotExists('hstore')Schema::createExtension('hstore') 启用 hstore 扩展。

标识符

标识符数据类型是 PostgreSQL 的新标准,用于自动生成值。您甚至可以指定数据库是否应该是唯一生成它们的(always = true),以防止意外覆盖。它们用于定义由数据库管理的主键或其他需要唯一性的任何类型的自动生成标识。

$table->identity(always: true)->primary();
$table->identity('uniqid');

国际产品编号

国际产品编号数据类型用于存储常见的商品编号类型并在保存之前验证它们。

// @see https://postgresql.ac.cn/docs/current/isn.html
$table->europeanArticleNumber13(string $column);
$table->internationalStandardBookNumber(string $column);
$table->internationalStandardBookNumber13(string $column);
$table->internationalStandardMusicNumber(string $column);
$table->internationalStandardMusicNumber13(string $column);
$table->internationalStandardSerialNumber(string $column);
$table->internationalStandardSerialNumber13(string $column);
$table->universalProductNumber(string $column);

注意

在之前,您需要使用 Schema::createExtensionIfNotExists('isn')Schema::createExtension('isn') 启用 isn 扩展。

标签树

ltree 数据类型将标签作为其在树中的位置存储。这提供了管理树的一种简单方法,与替代方案相比,它具有性能和复杂性的优势。

// @see https://postgresql.ac.cn/docs/current/ltree.html
$table->labelTree(string $column);

注意

在之前,您需要使用 Schema::createExtensionIfNotExists('ltree')Schema::createExtension('ltree') 启用 ltree 扩展。

向量

矢量类型可用于存储和搜索由 AI 提供商(如 OpenAI)创建的嵌入。

// @see https://github.com/pgvector/pgvector
$table->vector(string $column, int $dimensions = 1536);

注意

在之前,您需要使用 Schema::createExtensionIfNotExists('vector')Schema::createExtension('vector') 启用 vector 扩展。

注意

vector 扩展不是标准的 PostgreSQL 扩展,但在大多数 PostgreSQL 云服务中可用。您可以使用以下查询检查支持情况:SELECT * FROM pg_available_extensions WHERE name = 'vector'

XML

xml 数据类型可以用来存储 xml 文档。

// @see https://postgresql.ac.cn/docs/current/datatype-xml.html
$table->xml(string $column);

查询

解释

Laravel 具有获取您正在构建的任何查询的数据库查询计划的能力。只需在查询上调用 explain() 即可获取查询计划集合。

此行为已被扩展为更符合 PostgreSQL。存在多个(可选)参数用于 explain 语句,每个版本都不同。增强的 PostgreSQL 驱动程序将自动激活您 PostgreSQL 版本可用的所有选项。

DB::table('migrations')->where('batch', 1)->explain()->dd();

// Output:
// array:1 [
//  0 => """
//    Seq Scan on public.migrations  (cost=0.00..11.75 rows=1 width=524)\n
//      Output: id, migration, batch\n
//      Filter: (migrations.batch = 1)\n
//    Settings: search_path = 'public'\n
//    Planning Time: 0.370 ms
//    """
//]

此外,您还可以通过执行查询来获取查询计划。查询计划将扩展到有价值的运行时信息,如每个操作的计时和缓冲区读写统计信息。

DB::table('migrations')->where('batch', 1)->explain(analyze:true)->dd();

// Output:
// array:1 [
//  0 => """
//    Seq Scan on public.migrations  (cost=0.00..11.75 rows=1 width=524) (actual time=0.014..0.031 rows=1 loops=1)\n
//      Output: id, migration, batch\n
//      Filter: (migrations.batch = 1)\n
//      Buffers: shared hit=1\n
//    Settings: search_path = 'public'\n
//    Planning:\n
//      Buffers: shared hit=61\n
//    Planning Time: 0.282 ms\n
//    Execution Time: 0.100 ms
//    """
//]

全文搜索

PostgreSQL全文搜索实现支持许多可调整项,以优化全文搜索质量。在其最基本的形式中,您需要指定要搜索的列和搜索术语。

Book::whereFullText(['title', 'description'], 'PostgreSQL')->get();

但实现还提供了更多隐藏在第三个可选参数中的功能。有关全文搜索所有选项的更多信息,请阅读这篇文章:使用PostgreSQL 12优化全文搜索

语言

默认情况下,列和搜索术语在english语言中被还原为其基本形式,以查找例如单数或复数单词的结果。如果您的应用程序使用不同的语言,您可以将其更改为例如spanish,或者使用不进行还原的simple语言。

Book::whereFullText(['title', 'description'], 'PostgreSQL', ['language' => 'spanish'])->get();

搜索模式

您可以选择全文搜索的三种不同搜索模式,默认为plainto模式。根据您的需求,搜索术语可以完全不同地处理,给您大量自由来调整全文搜索以满足您的需求。

  • plainto:搜索术语中的所有单词至少必须在一列中存在一次。
    Book::whereFullText(['title', 'description'], 'PostgreSQL', ['mode' => 'plain'])->get();
  • phrase:搜索术语中的所有单词必须按精确相同的顺序出现在列中。
    Book::whereFullText(['title', 'description'], 'PostgreSQL database', ['mode' => 'phrase'])->get();
  • websearch:复杂的搜索术语,支持引用值、or关键字和-来排除一个单词,但没有括号。
    Book::whereFullText(['title', 'description'], '"PostgreSQL database" -MySQL', ['mode' => 'websearch'])->get();

权重

当您想要对全文搜索结果进行排序时,您需要一种方法来声明某些列比其他列更重要。使用权重选项,您为每个以A开头并以Z结尾的搜索列设置相关性。如果您想的话,您可以使用相同的相对权重多次,使某些列同等重要。

Book::whereFullText(['title', 'description'], '"PostgreSQL', ['weight' => ['A', 'B']])->get();

横向子查询连接

PostgreSQL支持高级横向子查询连接。最简单的解释是,您可以访问之前已选择表及其列,使其成为依赖子查询。您现在将能够执行与php中的foreach循环等效的连接,这将提供全新的可能性。

这是一个非常高级的结构,您可以在以下文章中了解更多信息

它们的使用方式与Laravel中的类似,但现在您使用crossJoinSubLateral而不是crossJoinSub,使用joinSubLateral而不是joinSub,使用leftJoinSubLateral而不是leftJoinSubLateral

一个非常常见的用例是在for-each循环概念中使用横向子查询,例如获取每个用户的最高价格的3个订单。

User::select('users.email', 'orders.*')
    ->leftJoinSubLateral(
        Order::whereColumn('orders.user_id', 'users.id')
            ->orderBy('price', 'desc')
            ->limit(3),
        'orders',
    );

从修改的行返回数据

有时获取INSERTUPDATEDELETE查询的影响行数据比仅获取受影响的行数更有用。PostgreSQL的RETURNING功能更改了数据操作语句的行为,以便在操作后SELECT行数据。

您可以在例如获取需要更新的用户列表时使用RETURNING。您不需要将所有用户选择到内存中,遍历它们并对每个进行操作,可以直接运行操作语句并获取所有受影响的行数据。一个典型的例子是报告哪些旧用户已被删除。

use Illuminate\Support\Facades\DB;

$inactiveUsers = DB::table('users')
    ->where('lastlogin_at', '<', now()->subYear())
    ->get();
foreach ($inactiveUsers as $inactiveUser) {
  $inactiveUser->delete();
}
dump('deleted Users', $inactiveUsers);

// do this instead:

$inactiveUsers = DB::table('users')
    ->where('lastlogin_at', '<', now()->subYear())
    ->deleteReturning();
dump('deleted Users', $inactiveUsers);

已添加以下修改查询(类似于其Laravel实现),这些查询返回受影响的行数据,而不是仅返回受影响的行数

  • deleteReturning
  • insertOrIgnoreReturning
  • insertReturning
  • insertUsingReturning
  • updateFromReturning
  • updateOrInsertReturning
  • updateReturning
  • upsertReturning

公共表表达式 (CTE)

您可以使用公用表表达式(CTE)或CTE对所有select、insert、update和delete方法进行编写辅助语句,以便在更大的查询中使用。需要传入CTE的别名、查询字符串或对象以及一个可选的选项数组,以对CTE进行更多控制。

$query->withExpression($as, $query, $options = []);

$lastLoginQuery = Login::query()
    ->selectRaw('user_id, MAX(created_at) AS last_login_at')
    ->groupBy('user_id');
User::query()
    ->withExpression('users_lastlogin', $lastLoginQuery)
    ->join('users_lastlogin', 'users_lastlogin.user_id', 'users.id')
    ->where('users_lastlogin.created_at', '>=', now()->subHour());

除了CTE的基本形式之外,这些可选设置也支持所有PostgreSQL选项。

注意

当您使用递归CTE时,始终使用cycle选项来防止由于数据中的循环而导致无限运行的查询。

通过游标延迟

如果您需要遍历大量行,您的内存可能不太足够。对于这些操作,Laravel提供了lazy()方法,该方法反复使用偏移分页,随着偏移量的增加而变得越来越慢。或者您可以使用更高效的lazyById,它使用主键来分页数据,这要高效得多,但仍需要执行相同的查询多次。

在PostgreSQL中,您可以通过使用游标来更高效地执行所有这些操作:查询只执行一次,应用程序可以在需要时请求更多行,因此不需要一次性将所有内容复制到内存中。

use Illuminate\Support\Facades\DB;

DB::transaction(function() {
    User::lazyByCursor()->each(function (User $user) {
        dump($user);
    });

    // Maximum 500 rows should be loaded into memory for every chunk.
    User::lazyByCursor(500)->each(function (User $user) {
        dump($user);
    });

    // Lazy loading rows also works for the query builder.
    DB::table('users')->where('active', true)->lazyByCursor()->each(function (object $user) {
        dump($user);
    });
});

WHERE 子句

任何/所有

PostgreSQL提供了非常方便的过滤函数,可以检查列是否与多个值匹配,而无需编写许多ANDOR条件。您可以说至少有一个值需要与' ANY'关键字一起匹配操作符。而对于ALL关键字,所有值都必须匹配。

// instead of:
$query->where('invoice', 'like', 'RV-%')->orWhere('invoice', 'like', 'RZ-%');
$query->where('json', '??', 'key1')->where('json', '??', 'key2');

// you can do:
$query->whereAnyValue('invoice', 'like', ['RV-%', 'RZ-%']);
$query->whereAllValues('json', '??', ['key1', 'key2']);
$query->whereAllValues($column, string $operator, iterable $values);
$query->whereNotAllValues($column, string $operator, iterable $values);
$query->orWhereAllValues($column, string $operator, iterable $values);
$query->orWhereNotAllValues($column, string $operator, iterable $values)
$query->whereAnyValue($column, string $operator, iterable $values);
$query->whereNotAnyValue($column, string $operator, iterable $values);
$query->orWhereAnyValue($column, string $operator, iterable $values);
$query->orWhereNotAnyValue($column, string $operator, iterable $values)

注意

自0.36.0版本以来,必须将后缀ValueValues添加到方法名称中,因为Laravel查询构建器也开始使用这些方法名称。

布尔值

由于Laravel始终将布尔值转换为整数,您有时可能会收到类似operator does not exist: boolean = integer的PostgreSQL错误。在大多数情况下,PostgreSQL足够智能,可以将值转换为类型,但在创建部分索引时,您可能会收到错误。要解决这个问题,您可以使用特殊的whereBoolean函数,这些函数不会将布尔值转换为01

$query->whereBoolean($column, bool $value);
$query->whereNotBoolean($column, bool $value);
$query->orWhereBoolean($column, bool $value);
$query->orWhereNotBoolean($column, bool $value);

类似

使用whereLike作用域,您可以在列和值之间进行不区分大小写的类似比较。

$query->whereLike($column, $value, $caseSensitive = false);
$query->orWhereLike($column, $value, $caseSensitive = false);

对称区间

Laravel已经提供了whereBetween子句,但您必须按顺序提供值,即较小的值是第一个数组项,较大的值是第二个数组项([4, 80])。使用PostgreSQL的BETWEEN SYMMETRIC关键字,您不再需要这样做,它将自动重新排序这些值。

现在您可以使用以下代码,例如,使用min/max值,而无需重新排序这些值,如果用户在输入时已经交换了它们的含义

$min = $request->integer('min');
$max = $request->integer('max');

// before:
$query->whereBetween('price', [min($min, $max), max($min, $max)]);

// now:
$query->whereBetweenSymmetric('price', [$min, $max]);
$query->whereBetweenSymmetric($column, iterable $values);
$query->whereNotBetweenSymmetric($column, iterable $values);
$query->orWhereBetweenSymmetric($column, iterable $values);
$query->orWhereNotBetweenSymmetric($column, iterable $values);

整数数组匹配

使用数组,您可以将整数集合存储为JSON列一样。但与JSON列不同的是,这些集合可以由intarray扩展进行查询,包括操作符&(AND)、|(OR)和!(NOT)。可以根据需要使用括号。例如,查询1& (2|3)匹配包含1并且还包含2或3的整数数组。

$query->whereIntegerArrayMatches($column, string $query);
$query->orWhereIntegerArrayMatches($column, string $query);

// The tags column should have values 3, 4, 5 or 6 and not 7.
$query->whereIntegerArrayMatches('tags', '3&4&(5|6)&!7');

ORDER BY

NULLS FIRST/LAST

默认情况下,NULL值在降序排序中排在所有值之前,在升序排序中排在所有值之后。当例如在表格中向用户显示时,这可能不是您喜欢的排序方式。使用“空值在开头/结尾”选项,您可以指定所需的精确行为

$query->orderBy($column, string $direction = 'asc'|'desc', string $nulls = 'default'|'first'|'last');
$query->orderByNullsFirst($column, string $direction = 'asc'|'desc', string $nulls = 'default'|'first'|'last');
$query->orderByNullsLast($column, string $direction = 'asc'|'desc', string $nulls = 'default'|'first'|'last');

// Sort the table by the age descending with all NULL values presented last.
$query->orderBy('age', 'desc', nulls: 'last');
$query->orderByNullsLast('age', 'desc');

警告

当使用非默认排序顺序时,您必须创建一个匹配的索引 - 标准索引不起作用!确切的索引 $table->index('age DESC NULLS LAST') 与查询匹配或 $table->index('age NULLS FIRT') 因为默认的列升序顺序。

向量相似度

使用orderByVectorSimilarity方法,您可以比较存储嵌入的列与其他嵌入。

$query->orderByVectorSimilarity($column, $vector, string $distance = 'cosine'|'l2');

// The five rows with the highest similarity to the provided embeddings.
$query->orderByVectorSimilarity('embeddings', [0.9569, 0.1113, 0.0107])->limit(5);

Eloquent

类型转换

某些 PostgreSQL 类型以字符串格式表示,Laravel 应用程序无法原生使用。为了使这些类型可使用,这些类型转换可以与您的 eloquent 模型一起使用

在保存时刷新数据

当您在迁移中使用 Laravel 的storedAs($expression)功能来在数据库中具有动态计算列或在触发器中更新这些列时,eloquent 的行为并不是您所期望的。在保存模型之后,这些计算属性在您的模型中不可用,您需要刷新它,因为 Laravel 只更新主键。

use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;

Schema::create('example', function (Blueprint $table) {
    $table->id();
    $table->string('text');
    $table->string('text_uppercase')->storedAs('UPPER(text)');
});

$example = Example::create(['text' => 'test']);
dump($example); // ['id' => 1, 'text' => 'test']

$example->refresh();
dump($example); // ['id' => 1, 'text' => 'test', 'text_uppercase' => 'TEST']

$example->fill(['text' => 'test2'])->save();
dump($example); // ['id' => 1, 'text' => 'test2']

$example->refresh();
dump($example); // ['id' => 1, 'text' => 'test', 'text_uppercase' => 'TEST2']

通过使用新的 RefreshDataOnSave trait,使用新的 RETURNING语句 来保存模型。当 Laravel 保存模型时,行中的任何更改都会自动反映在您的模型中

use Illuminate\Database\Eloquent\Model;
use Tpetry\PostgresqlEnhanced\Eloquent\Concerns\RefreshDataOnSave;

class Example extends Model
{
    use RefreshDataOnSave;

    // ...
}

$example = Example::create(['text' => 'test']);
dump($example); // ['id' => 1, 'text' => 'test', 'text_uppercase' => 'TEST']

$example->fill(['text' => 'test2'])->save();
dump($example); // ['id' => 1, 'text' => 'test2', 'text_uppercase' => 'TES2T']

日期格式

Laravel 迁移支持比标准 Y-m-d H:i:s 格式更多的日期:您可以使用改进的 timestampTz 日期格式,它正确处理时区,或者如果您想保存毫秒,可以选择保存毫秒。然而,直到您更改模型的 $dateFormat,标准的 eloquent 模型与这些扩展格式不会完美工作。但当你在一个表中混合不同的日期类型时,你可能会遇到不同的问题。已添加两个新特质来解决此问题

当您的表有->timestampTz()列时,应使用新的AutomaticDateFormat特质

use Illuminate\Database\Eloquent\Model;
use Tpetry\PostgresqlEnhanced\Eloquent\Concerns\AutomaticDateFormat;

class Example extends Model
{
    use AutomaticDateFormat;

    // ...
}

当您还存储某些->timestamp()->timestampTz()列的毫秒时,应使用新的AutomaticDateFormatWithMilliseconds特质

use Illuminate\Database\Eloquent\Model;
use Tpetry\PostgresqlEnhanced\Eloquent\Concerns\AutomaticDateFormatWithMilliseconds;

class Example extends Model
{
    use AutomaticDateFormatWithMilliseconds;

    // ...
}

注意

当您在一个表中混合带毫秒和不带毫秒的列时,不带毫秒的列可能会以您意想不到的方式表现:PostgreSQL 不是截断毫秒,而是将其四舍五入。当值被向上四舍五入时,您的时间戳将显示在将来。

表达式

Laravel 10 添加了使用查询构建器中的预制作表达式来生成特定于供应商的 SQL 的功能,如下所示,用于复杂操作

BlogVisit::select([
    'url',
    new TimestampBin('created_at', DateInterval::createFromDateString('5 minutes')),
    new Count('*'),
])->groupBy(
    'url',
    new TimestampBin('created_at', DateInterval::createFromDateString('5 minutes'))
);

我已经发布了很多表达式,这些表达式可以在 Laravel 支持的所有数据库中使用。但是,有些功能无法为所有数据库构建。因此,这里有一些特定的 PostgreSQL 表达式。

Uuid7

现在您可以直接在数据库中生成按时间排序的 UUIDv7 ID。使用 Str::orderedUuid() 的缺点是只能从 Laravel 中插入新行:您失去了使用 GUI、简单的 INSERT 查询或高效的 INSERT INTO ... SELECT 方法手动插入新行的能力。但所有这些方法都可以使用在数据库生成的 ID 上。

use Tpetry\PostgresqlEnhanced\Expressions\Uuid7;

Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->uuid()->default(new Uuid7())->unique();
    $table->text('text');
});        

破坏性变更

  • 1.0.0
    • Laravel 11.17 发布了新的 whereLikeorWhereLike 构建器方法,因此必须将其与 Laravel 的实现保持一致
      • 默认情况下,现在默认为不区分大小写的搜索,而不是区分大小写的搜索
      • 必须用 $caseInsensitive 参数替换 $caseSensitive 参数
  • 0.39.0 -> 0.40.0
    • 增强的 PostgreSQL 驱动程序 PHPStan 扩展现在与 PHPStan 扩展安装程序自动注册。当 PHPStan 崩溃时,需要删除扩展的手动注册,因为扩展已注册两次。
  • 0.35.0 -> 0.36.0
    • 由于它们现在与 Laravel 10.47 添加的新方法重叠,因此必须更改一些查询构建器方法。
      • whereAll -> whereAllValues
      • whereNotAll -> whereNotAllValues
      • orWhereAll -> orWhereAllValues
      • orWhereNotAll -> orWhereNotAllValues
      • whereAny -> whereAnyValue
      • whereNotAny -> whereNotAnyValue
      • orWhereAny -> orWhereAnyValue
      • orWhereNotAny -> orWhereNotAnyValue
  • 0.10.0 -> 0.11.0
    • ZeroDowntimeMigration 相关命名空间已从 Tpetry\PostgresqlEnhanced\Concerns 移动到 Tpetry\PostgresqlEnhanced\Schema\Concerns.
  • 0.12.0 -> 0.12.1
    • 所有返回语句的返回类型已从 array 更改为 Collection,以复制 Query\Builder::get() 方法签名。

贡献

如果您想为此包贡献代码,请首先提交一个问题。首先讨论想法、功能及其 API 非常有益于避免不必要的努力。

更新日志

有关最近更改的更多信息,请参阅 更新日志

安全漏洞

如果您发现任何与安全相关的问题,请通过电子邮件发送至 github@tpetry.me 而不是使用问题跟踪器。

许可

MIT 许可证 (MIT)。有关更多信息,请参阅 许可文件