Compatibilité MySQL + suppression de Redis comme dépendance requise
DbCompat (app/Support/DbCompat.php) : - like() → ilike (pgsql) ou like (mysql) - jsonRegexRaw() → data::text ~* ? (pgsql) ou CAST(data AS CHAR) REGEXP ? (mysql) - ftsRaw() → to_tsvector/plainto_tsquery (pgsql) ou null/fallback LIKE (mysql) - generatedJsonCol() → syntaxe colonne générée JSON selon le SGBD - generatedJsonNestedCol() → idem pour champs imbriqués Migrations : - create_releves_table : JSON/JSONB selon SGBD, colonnes générées adaptées, index GIN uniquement pour PostgreSQL Controllers : - LieuController (search + index) : ilike → DbCompat::like() - Admin\UserController (index) : ilike → DbCompat::like() - RechercheController : FTS + regex → DbCompat, fallback LIKE MySQL - ExportController : regex → DbCompat::jsonRegexRaw() UpdateService : - backupDatabase() : pg_dump (pgsql) ou mysqldump (mysql) - restoreBackup() : psql (pgsql) ou mysql (mysql) Docker : - docker-compose.yml : suppression Redis (plus requis) - docker-compose.mysql.yml : nouveau fichier pour dev MySQL - docker-compose.prod.yml : suppression Redis, DB_IMAGE configurable, CACHE_STORE/SESSION_DRIVER/QUEUE_CONNECTION → database par défaut .env.example : - DB_PORT commenté avec les deux valeurs (5432/3306) - CACHE_STORE et QUEUE_CONNECTION commentés (database par défaut) - Redis marqué optionnel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+7
-3
@@ -20,13 +20,15 @@ LOG_STACK=single
|
|||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# ── Base de données — choisir pgsql ou mysql ──────────────────────────────────
|
||||||
DB_CONNECTION=pgsql
|
DB_CONNECTION=pgsql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_PORT=5432
|
DB_PORT=5432 # PostgreSQL : 5432 | MySQL : 3306
|
||||||
DB_DATABASE=mesreleves
|
DB_DATABASE=mesreleves
|
||||||
DB_USERNAME=mesreleves
|
DB_USERNAME=mesreleves
|
||||||
DB_PASSWORD=secret
|
DB_PASSWORD=secret
|
||||||
|
|
||||||
|
# ── Sessions / Cache / Queue — driver "database" (pas de Redis requis) ────────
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
SESSION_ENCRYPT=false
|
SESSION_ENCRYPT=false
|
||||||
@@ -35,13 +37,15 @@ SESSION_DOMAIN=null
|
|||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
BROADCAST_CONNECTION=log
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
QUEUE_CONNECTION=database
|
QUEUE_CONNECTION=database # database (défaut) | redis (optionnel)
|
||||||
|
|
||||||
CACHE_STORE=database
|
CACHE_STORE=database # database (défaut) | redis (optionnel)
|
||||||
# CACHE_PREFIX=
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
MEMCACHED_HOST=127.0.0.1
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
# ── Redis (optionnel — non requis par défaut) ─────────────────────────────────
|
||||||
|
# Pour l'activer : changer CACHE_STORE et SESSION_DRIVER en "redis"
|
||||||
REDIS_CLIENT=phpredis
|
REDIS_CLIENT=phpredis
|
||||||
REDIS_HOST=127.0.0.1
|
REDIS_HOST=127.0.0.1
|
||||||
REDIS_PASSWORD=null
|
REDIS_PASSWORD=null
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
|
|||||||
use App\Enums\UserRole;
|
use App\Enums\UserRole;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Support\DbCompat;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\Rules\Enum;
|
use Illuminate\Validation\Rules\Enum;
|
||||||
@@ -22,9 +23,10 @@ class UserController extends Controller
|
|||||||
|
|
||||||
if ($request->filled('q')) {
|
if ($request->filled('q')) {
|
||||||
$q = trim($request->get('q'));
|
$q = trim($request->get('q'));
|
||||||
|
$like = DbCompat::like();
|
||||||
$query->where(fn ($wq) => $wq
|
$query->where(fn ($wq) => $wq
|
||||||
->where('name', 'ilike', "%{$q}%")
|
->where('name', $like, "%{$q}%")
|
||||||
->orWhere('email', 'ilike', "%{$q}%")
|
->orWhere('email', $like, "%{$q}%")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Enums\SourceStatus;
|
|||||||
use App\Models\Releve;
|
use App\Models\Releve;
|
||||||
use App\Models\Source;
|
use App\Models\Source;
|
||||||
use App\Services\GedcomExportService;
|
use App\Services\GedcomExportService;
|
||||||
|
use App\Support\DbCompat;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -51,14 +52,16 @@ class ExportController extends Controller
|
|||||||
|
|
||||||
if ($request->filled('q')) {
|
if ($request->filled('q')) {
|
||||||
$q = trim($request->get('q'));
|
$q = trim($request->get('q'));
|
||||||
$query->where(function ($wq) use ($q) {
|
$like = DbCompat::like();
|
||||||
$wq->where('nom', 'ilike', "%{$q}%")
|
$fts = DbCompat::ftsRaw();
|
||||||
->orWhere('prenom','ilike', "%{$q}%")
|
|
||||||
->orWhere('date_evenement', 'ilike', "%{$q}%")
|
$query->where(function ($wq) use ($q, $like, $fts) {
|
||||||
->orWhereRaw(
|
$wq->where('nom', $like, "%{$q}%")
|
||||||
"to_tsvector('french', data::text) @@ plainto_tsquery('french', ?)",
|
->orWhere('prenom', $like, "%{$q}%")
|
||||||
[$q]
|
->orWhere('date_evenement', $like, "%{$q}%");
|
||||||
);
|
if ($fts) {
|
||||||
|
$wq->orWhereRaw($fts, [$q]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +79,7 @@ class ExportController extends Controller
|
|||||||
$noms = collect($rows)->pluck('nom')->filter();
|
$noms = collect($rows)->pluck('nom')->filter();
|
||||||
if ($noms->isNotEmpty()) {
|
if ($noms->isNotEmpty()) {
|
||||||
$pattern = $noms->map(fn ($n) => preg_quote($n, '/'))->join('|');
|
$pattern = $noms->map(fn ($n) => preg_quote($n, '/'))->join('|');
|
||||||
$query->whereRaw("data::text ~* ?", [$pattern]);
|
$query->whereRaw(DbCompat::jsonRegexRaw('data'), [$pattern]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Http\Requests\StoreLieuRequest;
|
|||||||
use App\Http\Requests\UpdateLieuRequest;
|
use App\Http\Requests\UpdateLieuRequest;
|
||||||
use App\Models\Lieu;
|
use App\Models\Lieu;
|
||||||
use App\Models\LieuType;
|
use App\Models\LieuType;
|
||||||
|
use App\Support\DbCompat;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -20,9 +21,9 @@ class LieuController extends Controller
|
|||||||
|
|
||||||
$lieux = Lieu::with('lieuType')
|
$lieux = Lieu::with('lieuType')
|
||||||
->where(function ($query) use ($q) {
|
->where(function ($query) use ($q) {
|
||||||
$query->where('nom_long', 'ilike', "%{$q}%")
|
$query->where('nom_long', DbCompat::like(), "%{$q}%")
|
||||||
->orWhere('nom', 'ilike', "%{$q}%")
|
->orWhere('nom', DbCompat::like(), "%{$q}%")
|
||||||
->orWhere('code', 'ilike', "%{$q}%");
|
->orWhere('code', DbCompat::like(), "%{$q}%");
|
||||||
})
|
})
|
||||||
->orderBy('nom_long')
|
->orderBy('nom_long')
|
||||||
->limit(25)
|
->limit(25)
|
||||||
@@ -49,9 +50,9 @@ class LieuController extends Controller
|
|||||||
if ($request->filled('q')) {
|
if ($request->filled('q')) {
|
||||||
$q = trim($request->get('q'));
|
$q = trim($request->get('q'));
|
||||||
$query->where(function ($wq) use ($q) {
|
$query->where(function ($wq) use ($q) {
|
||||||
$wq->where('nom_long', 'ilike', "%{$q}%")
|
$wq->where('nom_long', DbCompat::like(), "%{$q}%")
|
||||||
->orWhere('nom', 'ilike', "%{$q}%")
|
->orWhere('nom', DbCompat::like(), "%{$q}%")
|
||||||
->orWhere('code', 'ilike', "%{$q}%");
|
->orWhere('code', DbCompat::like(), "%{$q}%");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Enums\SourceStatus;
|
|||||||
use App\Models\Lieu;
|
use App\Models\Lieu;
|
||||||
use App\Models\Releve;
|
use App\Models\Releve;
|
||||||
use App\Models\SourceType;
|
use App\Models\SourceType;
|
||||||
|
use App\Support\DbCompat;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
@@ -51,14 +52,17 @@ class RechercheController extends Controller
|
|||||||
// ── Recherche textuelle ──────────────────────────────────────────────
|
// ── Recherche textuelle ──────────────────────────────────────────────
|
||||||
if ($request->filled('q')) {
|
if ($request->filled('q')) {
|
||||||
$q = trim($request->get('q'));
|
$q = trim($request->get('q'));
|
||||||
$query->where(function ($wq) use ($q) {
|
$like = DbCompat::like();
|
||||||
$wq->where('nom', 'ilike', "%{$q}%")
|
$fts = DbCompat::ftsRaw();
|
||||||
->orWhere('prenom','ilike', "%{$q}%")
|
|
||||||
->orWhere('date_evenement', 'ilike', "%{$q}%")
|
$query->where(function ($wq) use ($q, $like, $fts) {
|
||||||
->orWhereRaw(
|
$wq->where('nom', $like, "%{$q}%")
|
||||||
"to_tsvector('french', data::text) @@ plainto_tsquery('french', ?)",
|
->orWhere('prenom', $like, "%{$q}%")
|
||||||
[$q]
|
->orWhere('date_evenement', $like, "%{$q}%");
|
||||||
);
|
|
||||||
|
if ($fts) {
|
||||||
|
$wq->orWhereRaw($fts, [$q]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,11 +70,10 @@ class RechercheController extends Controller
|
|||||||
if ($request->filled('lieu_id')) {
|
if ($request->filled('lieu_id')) {
|
||||||
$lieuNoms = $this->getLieuNoms($request->integer('lieu_id'));
|
$lieuNoms = $this->getLieuNoms($request->integer('lieu_id'));
|
||||||
if ($lieuNoms->isNotEmpty()) {
|
if ($lieuNoms->isNotEmpty()) {
|
||||||
// Recherche regex case-insensitive dans le JSONB text
|
|
||||||
$pattern = $lieuNoms
|
$pattern = $lieuNoms
|
||||||
->map(fn ($n) => preg_quote($n, '/'))
|
->map(fn ($n) => preg_quote($n, '/'))
|
||||||
->join('|');
|
->join('|');
|
||||||
$query->whereRaw("data::text ~* ?", [$pattern]);
|
$query->whereRaw(DbCompat::jsonRegexRaw('data'), [$pattern]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,21 +206,30 @@ class UpdateService
|
|||||||
throw new RuntimeException("Fichier introuvable : {$path}");
|
throw new RuntimeException("Fichier introuvable : {$path}");
|
||||||
}
|
}
|
||||||
|
|
||||||
$host = config('database.connections.pgsql.host');
|
$driver = config('database.default');
|
||||||
$port = config('database.connections.pgsql.port', 5432);
|
$conn = config("database.connections.{$driver}");
|
||||||
$db = config('database.connections.pgsql.database');
|
|
||||||
$user = config('database.connections.pgsql.username');
|
|
||||||
$password = config('database.connections.pgsql.password');
|
|
||||||
|
|
||||||
|
if ($driver === 'pgsql') {
|
||||||
$cmd = sprintf(
|
$cmd = sprintf(
|
||||||
'PGPASSWORD=%s psql -h %s -p %s -U %s %s < %s',
|
'PGPASSWORD=%s psql -h %s -p %s -U %s %s < %s',
|
||||||
escapeshellarg($password),
|
escapeshellarg($conn['password']),
|
||||||
escapeshellarg($host),
|
escapeshellarg($conn['host']),
|
||||||
(int) $port,
|
(int) ($conn['port'] ?? 5432),
|
||||||
escapeshellarg($user),
|
escapeshellarg($conn['username']),
|
||||||
escapeshellarg($db),
|
escapeshellarg($conn['database']),
|
||||||
escapeshellarg($path),
|
escapeshellarg($path),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
$cmd = sprintf(
|
||||||
|
'mysql -h %s -P %s -u %s %s %s < %s',
|
||||||
|
escapeshellarg($conn['host']),
|
||||||
|
(int) ($conn['port'] ?? 3306),
|
||||||
|
escapeshellarg($conn['username']),
|
||||||
|
$conn['password'] ? '-p' . escapeshellarg($conn['password']) : '',
|
||||||
|
escapeshellarg($conn['database']),
|
||||||
|
escapeshellarg($path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Artisan::call('down');
|
Artisan::call('down');
|
||||||
$this->exec($cmd);
|
$this->exec($cmd);
|
||||||
@@ -238,27 +247,35 @@ class UpdateService
|
|||||||
|
|
||||||
$filename = "db-before-{$version}-" . date('Ymd-His') . ".sql";
|
$filename = "db-before-{$version}-" . date('Ymd-His') . ".sql";
|
||||||
$path = "{$dir}/{$filename}";
|
$path = "{$dir}/{$filename}";
|
||||||
|
$driver = config('database.default');
|
||||||
|
$conn = config("database.connections.{$driver}");
|
||||||
|
|
||||||
$host = config('database.connections.pgsql.host');
|
if ($driver === 'pgsql') {
|
||||||
$port = config('database.connections.pgsql.port', 5432);
|
|
||||||
$db = config('database.connections.pgsql.database');
|
|
||||||
$user = config('database.connections.pgsql.username');
|
|
||||||
$password = config('database.connections.pgsql.password');
|
|
||||||
|
|
||||||
$cmd = sprintf(
|
$cmd = sprintf(
|
||||||
'PGPASSWORD=%s pg_dump -h %s -p %s -U %s %s > %s',
|
'PGPASSWORD=%s pg_dump -h %s -p %s -U %s %s > %s',
|
||||||
escapeshellarg($password),
|
escapeshellarg($conn['password']),
|
||||||
escapeshellarg($host),
|
escapeshellarg($conn['host']),
|
||||||
(int) $port,
|
(int) ($conn['port'] ?? 5432),
|
||||||
escapeshellarg($user),
|
escapeshellarg($conn['username']),
|
||||||
escapeshellarg($db),
|
escapeshellarg($conn['database']),
|
||||||
escapeshellarg($path),
|
escapeshellarg($path),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
$cmd = sprintf(
|
||||||
|
'mysqldump -h %s -P %s -u %s %s %s > %s',
|
||||||
|
escapeshellarg($conn['host']),
|
||||||
|
(int) ($conn['port'] ?? 3306),
|
||||||
|
escapeshellarg($conn['username']),
|
||||||
|
$conn['password'] ? '-p' . escapeshellarg($conn['password']) : '',
|
||||||
|
escapeshellarg($conn['database']),
|
||||||
|
escapeshellarg($path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$this->exec($cmd);
|
$this->exec($cmd);
|
||||||
|
|
||||||
if (! file_exists($path) || filesize($path) === 0) {
|
if (! file_exists($path) || filesize($path) === 0) {
|
||||||
throw new RuntimeException("La sauvegarde PostgreSQL a échoué (pg_dump disponible dans le container ?)");
|
throw new RuntimeException("La sauvegarde de la base de données a échoué ({$driver}).");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $path;
|
return $path;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers pour les différences de syntaxe SQL entre PostgreSQL et MySQL.
|
||||||
|
*/
|
||||||
|
class DbCompat
|
||||||
|
{
|
||||||
|
public static function isPgsql(): bool
|
||||||
|
{
|
||||||
|
return DB::connection()->getDriverName() === 'pgsql';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opérateur LIKE insensible à la casse */
|
||||||
|
public static function like(): string
|
||||||
|
{
|
||||||
|
return self::isPgsql() ? 'ilike' : 'like';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Caste une colonne JSON en texte brut */
|
||||||
|
public static function jsonToText(string $column): string
|
||||||
|
{
|
||||||
|
return self::isPgsql()
|
||||||
|
? "{$column}::text"
|
||||||
|
: "CAST({$column} AS CHAR)";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition de correspondance regex (insensible à la casse) sur une colonne JSON.
|
||||||
|
* Retourne la chaîne à passer à whereRaw() avec un seul binding `?`.
|
||||||
|
*/
|
||||||
|
public static function jsonRegexRaw(string $column = 'data'): string
|
||||||
|
{
|
||||||
|
return self::isPgsql()
|
||||||
|
? "{$column}::text ~* ?"
|
||||||
|
: "CAST({$column} AS CHAR) REGEXP ?";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition de recherche plein-texte.
|
||||||
|
* Retourne null pour MySQL (le caller doit prévoir un fallback LIKE).
|
||||||
|
*/
|
||||||
|
public static function ftsRaw(): ?string
|
||||||
|
{
|
||||||
|
return self::isPgsql()
|
||||||
|
? "to_tsvector('french', data::text) @@ plainto_tsquery('french', ?)"
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Syntaxe de la colonne générée stockée pour extraire un champ JSON de premier niveau */
|
||||||
|
public static function generatedJsonCol(string $jsonKey): string
|
||||||
|
{
|
||||||
|
return self::isPgsql()
|
||||||
|
? "data->>'$jsonKey'"
|
||||||
|
: "JSON_UNQUOTE(JSON_EXTRACT(data, '$.$jsonKey'))";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Syntaxe de la colonne générée stockée pour un champ JSON imbriqué (ex: date_evenement.valeur) */
|
||||||
|
public static function generatedJsonNestedCol(string $jsonPath): string
|
||||||
|
{
|
||||||
|
if (self::isPgsql()) {
|
||||||
|
// data->'date_evenement'->>'valeur'
|
||||||
|
$parts = explode('.', $jsonPath);
|
||||||
|
$last = array_pop($parts);
|
||||||
|
$chain = implode('', array_map(fn ($p) => "->'{$p}'", $parts));
|
||||||
|
return "data{$chain}->>'{$last}'";
|
||||||
|
}
|
||||||
|
// MySQL: $.date_evenement.valeur
|
||||||
|
return "JSON_UNQUOTE(JSON_EXTRACT(data, '$.{$jsonPath}'))";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +1,57 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Support\DbCompat;
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration
|
return new class extends Migration
|
||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('releves', function (Blueprint $table) {
|
$isPgsql = DbCompat::isPgsql();
|
||||||
|
|
||||||
|
Schema::create('releves', function (Blueprint $table) use ($isPgsql) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('source_id')->constrained()->cascadeOnDelete();
|
$table->foreignId('source_id')->constrained()->cascadeOnDelete();
|
||||||
$table->jsonb('data'); // champs variables selon source_type
|
// PostgreSQL : JSONB (binary JSON, indexable par GIN)
|
||||||
|
// MySQL : JSON
|
||||||
|
if ($isPgsql) {
|
||||||
|
$table->jsonb('data');
|
||||||
|
} else {
|
||||||
|
$table->json('data');
|
||||||
|
}
|
||||||
$table->foreignId('created_by')->constrained('users')->restrictOnDelete();
|
$table->foreignId('created_by')->constrained('users')->restrictOnDelete();
|
||||||
$table->foreignId('updated_by')->constrained('users')->restrictOnDelete();
|
$table->foreignId('updated_by')->constrained('users')->restrictOnDelete();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Colonnes générées stockées pour les champs les plus filtrés
|
// ── Colonnes générées stockées (syntaxe différente selon le SGBD) ──────
|
||||||
DB::statement("ALTER TABLE releves ADD COLUMN nom TEXT GENERATED ALWAYS AS (data->>'nom') STORED");
|
$nomExpr = DbCompat::generatedJsonCol('nom');
|
||||||
DB::statement("ALTER TABLE releves ADD COLUMN prenom TEXT GENERATED ALWAYS AS (data->>'prenom') STORED");
|
$prenomExpr = DbCompat::generatedJsonCol('prenom');
|
||||||
DB::statement("ALTER TABLE releves ADD COLUMN date_evenement TEXT GENERATED ALWAYS AS (data->'date_evenement'->>'valeur') STORED");
|
$dateEvtExpr = DbCompat::generatedJsonNestedCol('date_evenement.valeur');
|
||||||
|
|
||||||
// Index pour les colonnes générées
|
if ($isPgsql) {
|
||||||
|
DB::statement("ALTER TABLE releves ADD COLUMN nom TEXT GENERATED ALWAYS AS ({$nomExpr}) STORED");
|
||||||
|
DB::statement("ALTER TABLE releves ADD COLUMN prenom TEXT GENERATED ALWAYS AS ({$prenomExpr}) STORED");
|
||||||
|
DB::statement("ALTER TABLE releves ADD COLUMN date_evenement TEXT GENERATED ALWAYS AS ({$dateEvtExpr}) STORED");
|
||||||
|
} else {
|
||||||
|
DB::statement("ALTER TABLE releves ADD COLUMN nom VARCHAR(255) GENERATED ALWAYS AS ({$nomExpr}) STORED");
|
||||||
|
DB::statement("ALTER TABLE releves ADD COLUMN prenom VARCHAR(255) GENERATED ALWAYS AS ({$prenomExpr}) STORED");
|
||||||
|
DB::statement("ALTER TABLE releves ADD COLUMN date_evenement VARCHAR(255) GENERATED ALWAYS AS ({$dateEvtExpr}) STORED");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Index B-tree sur les colonnes générées ───────────────────────────
|
||||||
DB::statement('CREATE INDEX releves_nom_idx ON releves (nom)');
|
DB::statement('CREATE INDEX releves_nom_idx ON releves (nom)');
|
||||||
DB::statement('CREATE INDEX releves_prenom_idx ON releves (prenom)');
|
DB::statement('CREATE INDEX releves_prenom_idx ON releves (prenom)');
|
||||||
DB::statement('CREATE INDEX releves_date_evenement_idx ON releves (date_evenement)');
|
DB::statement('CREATE INDEX releves_date_evenement_idx ON releves (date_evenement)');
|
||||||
|
|
||||||
// Index GIN pour la recherche fulltext sur l'ensemble du JSONB
|
// ── Index GIN sur le JSON complet (PostgreSQL uniquement) ─────────────
|
||||||
|
if ($isPgsql) {
|
||||||
DB::statement('CREATE INDEX releves_data_gin_idx ON releves USING gin (data)');
|
DB::statement('CREATE INDEX releves_data_gin_idx ON releves USING gin (data)');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Développement local — MySQL 8 (alternative à PostgreSQL)
|
||||||
|
# Usage : docker compose -f docker-compose.mysql.yml up -d
|
||||||
|
# .env : DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mysql:8
|
||||||
|
container_name: mesreleves_db_mysql
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: mesreleves
|
||||||
|
MYSQL_USER: mesreleves
|
||||||
|
MYSQL_PASSWORD: secret
|
||||||
|
MYSQL_ROOT_PASSWORD: secret
|
||||||
|
MYSQL_CHARSET: utf8mb4
|
||||||
|
MYSQL_COLLATION: utf8mb4_unicode_ci
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
volumes:
|
||||||
|
- mysqldata:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-umesreleves", "-psecret"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysqldata:
|
||||||
+18
-25
@@ -1,4 +1,5 @@
|
|||||||
# Déploiement production — stack complète (app + db + redis + nginx)
|
# Déploiement production — stack complète (app + db + nginx)
|
||||||
|
# Pas de Redis — cache/sessions/queue utilisent la base de données
|
||||||
# Usage : docker compose -f docker-compose.prod.yml up -d
|
# Usage : docker compose -f docker-compose.prod.yml up -d
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
@@ -10,25 +11,20 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
APP_ENV: production
|
APP_ENV: production
|
||||||
APP_KEY: ${APP_KEY}
|
APP_KEY: ${APP_KEY}
|
||||||
DB_CONNECTION: pgsql
|
DB_CONNECTION: ${DB_CONNECTION:-pgsql}
|
||||||
DB_HOST: db
|
DB_HOST: db
|
||||||
DB_PORT: 5432
|
DB_PORT: ${DB_PORT:-5432}
|
||||||
DB_DATABASE: ${DB_DATABASE:-mesreleves}
|
DB_DATABASE: ${DB_DATABASE:-mesreleves}
|
||||||
DB_USERNAME: ${DB_USERNAME:-mesreleves}
|
DB_USERNAME: ${DB_USERNAME:-mesreleves}
|
||||||
DB_PASSWORD: ${DB_PASSWORD}
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
CACHE_STORE: redis
|
CACHE_STORE: database
|
||||||
SESSION_DRIVER: redis
|
SESSION_DRIVER: database
|
||||||
QUEUE_CONNECTION: redis
|
QUEUE_CONNECTION: database
|
||||||
REDIS_HOST: redis
|
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
|
||||||
condition: service_started
|
|
||||||
volumes:
|
volumes:
|
||||||
# Le code est monté depuis l'hôte : les mises à jour ne nécessitent pas de rebuild image
|
|
||||||
- .:/var/www/html
|
- .:/var/www/html
|
||||||
# Le storage est un volume nommé (uploads, logs, sessions — persisté indépendamment du code)
|
|
||||||
- app_storage:/var/www/html/storage
|
- app_storage:/var/www/html/storage
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
@@ -50,35 +46,32 @@ services:
|
|||||||
- internal
|
- internal
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:18-alpine
|
image: ${DB_IMAGE:-postgres:18-alpine}
|
||||||
container_name: mesreleves_db
|
container_name: mesreleves_db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
# PostgreSQL
|
||||||
POSTGRES_DB: ${DB_DATABASE:-mesreleves}
|
POSTGRES_DB: ${DB_DATABASE:-mesreleves}
|
||||||
POSTGRES_USER: ${DB_USERNAME:-mesreleves}
|
POSTGRES_USER: ${DB_USERNAME:-mesreleves}
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
# MySQL (ignoré par PostgreSQL)
|
||||||
|
MYSQL_DATABASE: ${DB_DATABASE:-mesreleves}
|
||||||
|
MYSQL_USER: ${DB_USERNAME:-mesreleves}
|
||||||
|
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql
|
- dbdata:/var/lib/postgresql # PostgreSQL
|
||||||
|
# Pour MySQL, remplacer par : - dbdata:/var/lib/mysql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-mesreleves}"]
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-mesreleves} || mysqladmin ping -h localhost -u${DB_USERNAME:-mesreleves} -p${DB_PASSWORD}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: mesreleves_redis
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- redisdata:/data
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
dbdata:
|
||||||
redisdata:
|
|
||||||
app_storage:
|
app_storage:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
+1
-11
@@ -1,4 +1,4 @@
|
|||||||
# Développement local — PostgreSQL + Redis uniquement
|
# Développement local — PostgreSQL uniquement (pas de Redis requis)
|
||||||
# L'application PHP tourne avec `php artisan serve` en dehors de Docker
|
# L'application PHP tourne avec `php artisan serve` en dehors de Docker
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
@@ -19,15 +19,5 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: mesreleves_redis
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
volumes:
|
|
||||||
- redisdata:/data
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
redisdata:
|
|
||||||
|
|||||||
Reference in New Issue
Block a user