Laravel + (MySQL / PostgreSQL + PostGIS + TimescaleDB)方案

Laravel 的设计天生支持多数据库连接和条件性功能,完全可以构建一个混合兼容方案:同一个 Laravel 系统,根据环境配置(e.g., .env 文件)自动切换底层存储(MySQL vs. PostgreSQL + PostGIS + TimescaleDB),ORM(Eloquent)层面保持一致,在 MySQL 版本中优雅“降级”不支持的功能(如时空查询用 fallback)。这样,现有的 MySQL 部署可以零迁移继续跑,新部署无缝升级到 PostgreSQL 栈。一套代码,多套实例,维护成本低。

基于 Laravel 11+(2025年标准)。整个方案的开发时间约1-2周(原型),测试1周。核心是抽象层:用 trait/service 封装差异,Eloquent 模型统一接口。

1. 方案核心原则

  • 配置驱动:用 .env 变量(如 DB_CONNECTION=mysql 或 pgsql)切换连接;另一个变量 DB_FEATURES=full(PostgreSQL)或 basic(MySQL)控制功能开关。
  • ORM 兼容:Eloquent 原生支持多连接(config/database.php 定义),模型用动态查询 builder。
  • 功能降级:MySQL 版限制高级时空查询(e.g., 空间距离用 PHP 计算 fallback),Timescale/PostGIS 只在 PostgreSQL 加载。
  • 部署实例:不同服务器/环境用不同 .env(e.g., staging=MySQL,production=PostgreSQL)。
  • 迁移策略:现有 MySQL 数据不变;新数据可选同步(用 Job 增量迁)。

2. 环境配置(config/database.php)

定义多连接,Laravel 自动根据 DB_CONNECTION 切换。

php

'connections' => [
    'mysql' => [
        'driver' => 'mysql',
        'host' => env('DB_HOST', '127.0.0.1'),
        'port' => env('DB_PORT', '3306'),
        'database' => env('DB_DATABASE', 'forge'),
        'username' => env('DB_USERNAME', 'forge'),
        'password' => env('DB_PASSWORD', ''),
        // ... 其他 MySQL 配置
    ],
    'pgsql' => [
        'driver' => 'pgsql',
        'host' => env('DB_HOST', '127.0.0.1'),
        'port' => env('DB_PORT', '5432'),
        'database' => env('DB_DATABASE', 'forge'),
        'username' => env('DB_USERNAME', 'forge'),
        'password' => env('DB_PASSWORD', ''),
        'schema' => 'public',
        // PostGIS/TimescaleDB 自动加载(扩展在 DB 侧配置)
    ],
],
'default' => env('DB_CONNECTION', 'mysql'),  // 动态切换
  • .env 示例:
    • MySQL 部署:DB_CONNECTION=mysql、DB_FEATURES=basic
    • PostgreSQL 部署:DB_CONNECTION=pgsql、DB_FEATURES=full

3. ORM 层兼容实现(Eloquent 模型)

用Trait 封装查询逻辑:基础方法统一,高级功能条件加载。核心模型 CorpusEvent 支持时空事件。

  • 安装包(Composer,根据配置可选):bash
composer require grimzy/laravel-mysql-spatial  # MySQL 空间 fallback
composer require clickbar/laravel-magellan  # PostGIS(仅 pgsql 加载)
# TimescaleDB 无需包,用 raw SQL
  • Trait 示例(app/Traits/SpatiotemporalTrait.php):php
trait SpatiotemporalTrait {
    public function scopeInTimeWindow($query, $startSjd, $endSjd) {
        return $query->whereBetween('sjdnp64', [$startSjd, $endSjd]);
    }

    public function scopeNear($query, $lat, $long, $radiusKm) {
        $connection = config('database.default');
        if ($connection === 'pgsql' && config('database.features') === 'full') {
            // PostGIS 高级:用 ST_DWithin
            return $query->whereRaw(
                'ST_DWithin(geo::geography, ST_MakePoint(?, ?)::geography, ?)',
                [$long, $lat, $radiusKm * 1000]
            );
        } else {
            // MySQL 降级:用 Haversine 公式(PHP 计算或 raw SQL)
            return $query->whereRaw(
                '(6371 * acos(cos(radians(?)) * cos(radians(geo_lat)) * cos(radians(geo_long) - radians(?)) + sin(radians(?)) * sin(radians(geo_lat)))) <= ?',
                [$lat, $long, $lat, $radiusKm]
            );  // 需在表加 geo_lat/geo_long 列
        }
    }

    public function scopeAggregateTimeBucket($query, $interval = '1 day') {
        $connection = config('database.default');
        if ($connection === 'pgsql' && config('database.features') === 'full') {
            // TimescaleDB 聚合
            return $query->selectRaw("time_bucket(?, sjdnp64) as bucket, AVG(sentiment) as avg_mood", [$interval])
                         ->groupBy('bucket');
        } else {
            // MySQL 降级:用 DATE_TRUNC 或手动分组
            return $query->selectRaw("DATE(sjdnp64 / 86400) as bucket, AVG(sentiment) as avg_mood")  // 简化
                         ->groupBy('bucket');
        }
    }
}
  • 模型示例(app/Models/CorpusEvent.php):php
use Illuminate\Database\Eloquent\Model;
use App\Traits\SpatiotemporalTrait;

class CorpusEvent extends Model {
    use SpatiotemporalTrait;

    protected $casts = [
        'geo' => 'array',  // MySQL: JSON 坐标;pgsql: Point 类型(Magellan 自动)
        'event' => 'array',  // JSON 事件
    ];

    // 迁移时,根据连接创建不同 schema
    protected static function boot() {
        parent::boot();
        if (config('database.default') === 'pgsql') {
            // PostGIS 类型 cast
            static::addGlobalScope(new \Clickbar\Magellan\Eloquent\Builder());
        }
    }
}

4. 迁移与 Schema 兼容(数据库迁移)

用 Laravel Migration 条件创建表:MySQL 版简单列,PostgreSQL 版加几何/hypertable。

  • Migration 示例(database/migrations/create_corpus_events_table.php):php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;

class CreateCorpusEventsTable extends Migration {
    public function up() {
        Schema::create('corpus_events', function (Blueprint $table) {
            $table->id();
            $table->bigInteger('sjdnp64');
            $table->json('event');  // 通用 JSON
            $table->text('raw_text');
            $table->timestamps();

            if (config('database.default') === 'pgsql' && config('database.features') === 'full') {
                // PostGIS 几何列
                $table->geometry('geo');  // 需要 Magellan 或 raw
                // Timescale hypertable
                DB::statement("SELECT create_hypertable('corpus_events', 'sjdnp64');");
                DB::statement("CREATE INDEX idx_geo ON corpus_events USING GIST (geo);");
            } else {
                // MySQL fallback: 分离 lat/long
                $table->decimal('geo_lat', 10, 7)->nullable();
                $table->decimal('geo_long', 10, 7)->nullable();
                DB::statement("CREATE SPATIAL INDEX idx_geo ON corpus_events (geo_point);");  // MySQL Spatial
            }
        });
    }
}
  • 运行:php artisan migrate —— 根据 .env 自动适配。

5. ETL 与 Job 兼容

  • 用 Laravel Queue(Redis)分批处理语料提取。
  • 在 Job 中检查连接:PostgreSQL 用 PostGIS 函数提取坐标,MySQL 用 PHP GeoPHP 库 fallback。
  • 示例:ExtractEventJob 中加 if (DB::connection()->getPdo()->getAttribute(PDO::ATTR_DRIVER_NAME) === ‘pgsql’) { /* 高级 */ } else { /* 基本 */ }。

6. API/Controller 层统一

  • Controller 用模型方法:e.g., $events = CorpusEvent::near($lat, $long, $radius)->inTimeWindow($start, $end)->get(); —— 内部自动降级。
  • 响应中加标志:{ “data”: […], “features”: config(‘database.features’) }(MySQL 版提示“高级查询受限”)。

7. 优缺点 & 部署建议

方面优点缺点
开发一套代码,多环境;Eloquent 无缝。需写条件逻辑(~20%代码),测试双环境。
性能PostgreSQL 部署获全功能;MySQL 旧部署零改。MySQL 版时空查询慢 5x(用缓存补)。
维护渐进升级:先 MySQL 跑,数据成熟再迁。功能开关易忘(用 config 缓存)。
  • 部署:用 Laravel Forge/Envoyer,一键切换 .env。现有 MySQL 实例不变,新实例 Docker 跑 PostgreSQL 栈。
  • 测试:用 Pest/PHPUnit 双连接测试(testMysql() vs. testPgsql())。

这个方案让系统“即插即用”——现有 MySQL 部署继续,新项目直接 full-stack。

发表回复

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

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理