使用 Eloquent 解析 PostgreSQL HSTORE 字段教程
在 Laravel 项目中,PostgreSQL 的 HSTORE 类型提供了一种灵活存储键值对数据的方式,非常适用于不需要严格模式定义但又需要查询能力的场景。本文将详细介绍如何通过 Eloquent ORM 优雅地读取、写入和查询 HSTORE 字段,包括类型转换、访问器、修改器以及原生的查询写法。
准备工作
1. 启用 HSTORE 扩展
在 PostgreSQL 中,HSTORE 是一个扩展模块,需要先激活。可以在迁移或直接在数据库中执行以下 SQL:
CREATE EXTENSION IF NOT EXISTS hstore;
建议在迁移文件中使用 DB::statement 来创建扩展,确保部署时自动生效。
2. 创建包含 HSTORE 字段的迁移
Laravel 的 Schema 构建器没有原生支持 hstore 类型,因此需要使用原始 SQL 定义字段。以下示例创建一张 products 表,其中 attributes 字段为 HSTORE 类型:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('sku')->unique();
// 使用 raw 方法添加 hstore 字段
$table->raw("attributes hstore");
$table->timestamps();
});
// 为 attributes 字段添加 GIN 索引,提升查询性能
DB::statement('CREATE INDEX idx_products_attributes ON products USING GIN (attributes)');
}
public function down()
{
Schema::dropIfExists('products');
}
};模型配置
1. 自定义 Cast 类
Eloquent 默认不支持直接转换 HSTORE 为数组。我们可以创建一个自定义的 CastsAttributes 类,实现 get 和 set 方法:
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Facades\Log;
class HstoreCast implements CastsAttributes
{
/**
* 将数据库中的 HSTORE 字符串转换为数组
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return array|null
*/
public function get($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return [];
}
// HSTORE 在 PHP 中是以字符串形式返回,例如 "key1=>value1, key2=>value2"
// 使用 PostgreSQL 的 hstore_to_json 或手动解析
// 这里使用 pg_fetch_array 或正则解析,但更推荐在 get 中使用 hstore_to_json 函数
// 注意:如果连接是 PDO,直接获取的字符串格式为:key1=>value1, key2=>value2
// 我们可以借助 PostgreSQL 的 hstore_to_json 转换
// 但为了简化,此处使用自定义解析函数
return $this->parseHstore($value);
}
/**
* 将数组转换为 HSTORE 字符串存入数据库
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return string
*/
public function set($model, string $key, $value, array $attributes)
{
if (is_null($value) || empty($value)) {
return null;
}
// 将关联数组转换为 HSTORE 字符串格式
$pairs = [];
foreach ($value as $k => $v) {
// 键和值都需要用双引号包裹,并转义内部双引号
$k = str_replace('"', '\\"', $k);
$v = str_replace('"', '\\"', $v);
$pairs[] = sprintf('"%s" => "%s"', $k, $v);
}
return implode(', ', $pairs);
}
/**
* 解析 HSTORE 字符串为数组
*
* @param string $hstore
* @return array
*/
private function parseHstore(string $hstore): array
{
$result = [];
// 匹配键值对:key=>value
// 键值可能被双引号包裹,也可能没有
// 简单处理:使用正则
preg_match_all('/"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"\s*=>\s*"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"/', $hstore, $matches);
if (!empty($matches[0])) {
for ($i = 0; $i < count($matches[0]); $i++) {
$key = stripslashes($matches[1][$i]);
$value = stripslashes($matches[2][$i]);
$result[$key] = $value;
}
}
return $result;
}
}2. 在模型中使用 Cast
在 Product 模型中添加 $casts 属性,指向自定义的 Cast 类:
<?php
namespace App\Models;
use App\Casts\HstoreCast;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = ['name', 'sku', 'attributes'];
protected $casts = [
'attributes' => HstoreCast::class,
];
}现在,当你从数据库中获取 attributes 字段时,它将自动转换为 PHP 数组;写入时,数组也会自动转换为 HSTORE 字符串。
CRUD 操作示例
1. 创建记录
$product = Product::create([
'name' => '红色T恤',
'sku' => 'TSHIRT-RED-001',
'attributes' => [
'color' => '红色',
'size' => 'L',
'material' => '棉',
],
]);
// attributes 字段会自动转为 hstore 存入数据库
echo $product->attributes['color']; // 输出:红色2. 读取和更新
$product = Product::find(1); // 读取特定键 echo $product->attributes['size']; // L // 修改某个键 $attrs = $product->attributes; $attrs['size'] = 'XL'; $product->attributes = $attrs; $product->save(); // 或者直接赋值数组 $product->attributes = array_merge($product->attributes, ['color' => '蓝色']); $product->save();
3. 查询:使用 whereRaw
HSTORE 支持丰富的操作符,如 ?(存在键)、?&(所有键存在)、?|(任一键存在)、=>(键值对匹配)。在 Eloquent 中可以使用 whereRaw 执行原生条件:
// 查找包含 'color' 键的产品
$products = Product::whereRaw("attributes ? 'color'")->get();
// 查找 color = '红色' 的产品
$products = Product::whereRaw("attributes -> 'color' = '红色'")->get();
// 或者使用 @> 操作符:包含指定键值对
$products = Product::whereRaw("attributes @> hstore('color', '红色')")->get();
// 查找 color 为红色且 size 为 L 的产品
$products = Product::whereRaw("attributes @> hstore('color', '红色') AND attributes @> hstore('size', 'L')")->get();
// 查找包含 color 和 size 键的产品(同时存在)
$products = Product::whereRaw("attributes ?& ARRAY['color', 'size']")->get();可以将这些常见的查询封装到模型中,例如添加 scope:
// 在 Product 模型中
public function scopeWhereHstoreContains($query, $key, $value)
{
return $query->whereRaw("attributes @> hstore(?, ?)", [$key, $value]);
}
public function scopeWhereHstoreHasKey($query, $key)
{
return $query->whereRaw("attributes ? ?", [$key]);
}
// 使用
$products = Product::whereHstoreContains('color', '红色')->get();
$products = Product::whereHstoreHasKey('material')->get();性能与替代方案
HSTORE 字段仅限于字符串键值对,且不能嵌套。如果应用需要存储更复杂的数据结构(如 JSON 嵌套对象、布尔值、数字),建议使用 PostgreSQL 的 JSONB 类型。Laravel 自版本 5.6 起原生支持 JSON 字段类型,并可直接使用 where 数组语法进行查询,无需自定义 Cast。
如果你已经使用了 HSTORE 且需要迁移到 JSONB,可以通过迁移添加 JSONB 列并编写数据转换脚本。在性能方面,两者都支持 GIN 索引,JSONB 功能更丰富,是更通用的选择。
常见问题与注意事项
- HSTORE 字符串格式:从数据库读取的原始字符串如
"color"=>"红色", "size"=>"L",自定义 Cast 中的parseHstore方法需要正确解析,建议在真实项目中直接使用 PostgreSQL 的hstore_to_json函数:hstore_to_json(attributes)::json,然后通过json_decode转换为数组,避免自行解析。但这需要修改查询或模型访问器。 - 空值处理:HSTORE 字段可以为 NULL,也可以为空字符串表示无数据。通过 Cast 类,我们将 NULL 转换为空数组。
- 转义字符:在
set方法中,如果键或值包含双引号或反斜杠,需要进行转义。上述例子已经处理了双引号的转义。 - 查找键不存在:直接访问数组中的不存在的键会引发错误,建议使用
Arr::get()或??运算符。
完整示例代码
将上述所有代码整合,创建一个简易的 Demo 模型:
// app/Models/Product.php
<?php
namespace App\Models;
use App\Casts\HstoreCast;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = ['name', 'sku', 'attributes'];
protected $casts = ['attributes' => HstoreCast::class];
// 作用域封装
public function scopeWhereHstoreContains($query, $key, $value)
{
return $query->whereRaw("attributes @> hstore(?, ?)", [$key, $value]);
}
public function scopeWhereHstoreHasKey($query, $key)
{
return $query->whereRaw("attributes ? ?", [$key]);
}
}// 使用示例
$product = Product::whereHstoreContains('color', '红色')->first();
if ($product) {
echo "产品名称:" . $product->name;
// 修改尺寸
$attr = $product->attributes;
$attr['size'] = 'M';
$product->attributes = $attr;
$product->save();
}通过以上步骤,你可以在 Laravel Eloquent 中方便地解析和操作 PostgreSQL HSTORE 字段。虽然需要额外的自定义 Cast 类,但一旦建立,后续开发体验与操作普通数组无异。如果你更倾向于减少维护成本,建议在新项目中优先考虑 JSONB 字段。