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。
