bitsmind / graphsql
GraphSql是一种类似于Graphql的语法方法,可以轻松地从SQL数据库中读取数据。它建立在Laravel Eloquent ORM之上。
Requires
- php: ^8.0
README
概述
GraphSql是一种类似于Graphql的语法方法,可以轻松地从SQL数据库中读取数据。它建立在Laravel Eloquent ORM之上。
在构建API路由时,我们通常面临一个困境。我们需要为不同的目的构建多个API,但数据来自相同的数据库表。假设对于具有10列的products
表,我们的前端应用程序中有2个列表。列表1仅显示name
和image
。列表2显示name
和description
。在这种情况下,我们可能构建2个API来仅返回特定字段,或者构建单个API来返回所有字段。构建2个API需要更长的时间。如果我们构建一个API,我们看到只需要两个字段,但我们正在返回所有10个字段。随着应用程序的增长,这个问题会扩大。
想象一下,你有一个工具,你可以从前端请求后端需要的字段,如{name,image}
。API将返回包含name
和image
字段的商品列表,或者请求{name,description}
以仅获取name
和description
,只需一个商品列表API。
这就是GraphSql
的功能。
GraphSql仅限于单个表吗?
哈哈,这里是,我们还可以请求相关表中的附加数据。想象一下,我们需要包含每个产品类别名称的产品列表。然后我们请求{name,image,category{name}}
。API将返回包含每个product
及其仅具有字段name
的category
的产品列表。
或者,包含其变体(表:product_variations
)的产品列表,variations{*}
返回所有字段。
或者,包含其变体计数(表:product_variations
),variations.count
。
我们可以在节点图字符串中添加条件,例如,variations(status=1,color=Blue){*}
返回状态为1且颜色为Blue的变体。允许的运算符:=
,!=
,>=
,<=
,>
,<
。
或者,variations(status=1,color=Blue).count
返回单个产品的状态为1且颜色为Blue的变体的variations_count
。或者,variations(status=1,color=Blue).sum.sale
返回单个产品的状态为1且颜色为Blue的variations_sum_sale
。
示例
API
product/list?graph={name,image,category{name}}
响应中的数据
[
{
name:"Pressure Cooker",
image:"/image/pressure_cooker.jpg",
category_id:1,
category:{
id: 1,
name:"Home Appliance"
}
},
.
.
.
]
不用担心输出中的category_id
和id
。我们稍后再讨论。
API
product/list?graph={name,image,category{name},variations{*}}
响应中的数据
[
{
name:"Pressure Cooker",
image:"/image/pressure_cooker.jpg",
category_id:1,
category:{
id: 1,
name:"Home Appliance"
},
variations:[
{
id:10,
color:"Red",
size:"Small",
price:2500,
status:1,
created_at: ...,
updated_at: ...,
}
.
.
.
]
},
.
.
.
]
API
product/list?graph={name,image,category{name},variations(status=1,color=Blue){*}}
响应中的数据
[
{
name:"Pressure Cooker",
image:"/image/pressure_cooker.jpg",
category_id:1,
category:{
id: 1,
name:"Home Appliance"
},
variations:[
{
id:12,
color:"Blue",
size:"Small",
price:2500,
status:1,
created_at: ...,
updated_at: ...,
}
.
.
.
]
},
.
.
.
]
API
product/list?graph={name,variations.count,variations.sum.sale}
响应中的数据
[
{
name:"Pressure Cooker",
variations_count: 4,
variations_sum_sale: "200"
},
{
name:"Induction Cooker",
variations_count: 2,
variations_sum_sale: "80"
},
.
.
.
]
这里发生了什么?
我们心中产生了几个问题。比如,如何获得category
表的数据?或者,如何请求variations{*}
节点,从product_variations
表中获取数据?
简单的回答:GraphSql使用Eloquent关系来实现这一点
app/Models/Product.php
public function category():BelongsTo
{
return $this->belongsTo(Category::class, 'category_id', 'id');
}
public function variations (): HasMany
{
return $this->hasMany(ProductVariation::class, 'product_id', 'id');
}
现在有道理了?这就是它们来自哪里。
安装
先决条件
- Laravel应用程序最小版本8
- 模型:User,Category,Product,ProductVariation,CartItem等
- 定义了适当的Eloquent关系
注意
- 图字符串:
{name,image,category{name},variations(status=1,color=Blue){*}}
- 节点:
category{name}
,variations(status=1,color=Blue){*}
- 节点标题:
category
,variations
- 节点属性:
*
,name
等 - 节点条件:
status=1
,color=Blue
- 节点标题是模型中为相关表定义的方法名称
- 节点属性/属性是列名
- 特殊节点属性:
*
表示所有列,_timestamps
表示created_at
和updated_at
列
- 在
routes/api.php
中创建路由use App\Http\Controllers\ProductController; use Illuminate\Support\Facades\Route; Route::prefix('/product')->group(function () { Route::get('/list', [ProductController::class, 'getList']); Route::get('/{id}', [ProductController::class, 'getSingle']); });
- 在
app/Http/Controllers/ProductController.php
中添加方法namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Http\Services\ProductService; class ProductController extends Controller { function __construct (private readonly ProductService $service) {} public function getList () { return response()->json( $this->service->getList()); } public function getSingle ($id) { return response()->json( $this->service->getProduct($id)); }
- 在
app/Http/Services/ProductService.php
中添加方法namespace App\Http\Services; class ProductService extends Service { public function getList (): array { try { $dbQuery = Product::get(); return [ 'success' => true, 'data' => ['products' => $products] ]; } catch (\Exception $exception) { return [ 'success' => false, 'message' => $exception->getMessage() ]; } } public function getSingle ($id): array { try { $dbQuery = Product::find($id); return [ 'success' => true, 'data' => ['product' => $product] ]; } catch (\Exception $exception) { return [ 'success' => false, 'message' => $exception->getMessage() ]; } }
现在让我们看看我们得到了什么
php artisan serve --port=8800
API
http://127.0.0.1:8800/api/product/list
响应
{
'success': true,
'data': {
'products': [
{
id:1,
name:"Pressure Cooker",
description:"Description ...",
image:"/image/pressure_cooker.jpg",
category_id:1,
brand:"Hitachi",
status:1,
tags:"pressure,cooker,...",
created_at: ...,
updated_at: ...,
},
.
.
.
]
}
}
让我们实现 GraphSql。
- 安装 GraphSql
composer require bitsmind/graphsql
- 将新表
graph_sql_keys
迁移到数据库。我们稍后再讨论这个问题。php artisan migrate
- 更新
app/Http/Services/ProductService.php
use Bitsmind\GraphSql\QueryAssist; class ProductService extends Service { use QueryAssist; public function getList (): array { try { $query = [ 'graph' => '{*}' // Use necessary graph string here. ]; $dbQuery = Product::query(); $dbQuery = $this->queryGraphSQL($dbQuery, $query, new Product); $products = $dbQuery->get(); return [ 'success' => true, 'data' => ['products' => $products] ]; } catch (\Exception $exception) { return [ 'success' => false, 'message' => $exception->getMessage() ]; } } . . . }
现在让我们看看我们得到了什么
API
http://127.0.0.1:8800/api/product/list
返回与之前相同的内容。
现在让我们尝试图形字符串并检查我们得到的结果
{*}
{name,image}
{id,name,image,_timestamps}
{name,image,category{*}}
{name,image,category{name}}
{name,image,category{name},variations{*}}
{name,image,category{name},variations.count}
{name,image,category{name},variations.sum.sale}
{name,image,category{name},variations(status=1).count}
- 你的想象力是无限的
让我们从 API 查询参数中获取字符串
- 更新
app/Http/Controllers/ProductController.php
namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Http\Services\ProductService; use Illuminate\Http\Request; class ProductController extends Controller { function __construct (private readonly ProductService $service) {} public function getList (Request $request): JsonResponse { return response()->json( $this->service->getList( $request->query())); } public function getSingle ($id, Request $request) { return response()->json( $this->service->getProduct($id, $request->query())); }
- 更新
app/Http/Services/ProductService.php
use Bitsmind\GraphSql\QueryAssist; class ProductService extends Service { use QueryAssist; public function getList (array $query): array { try { $dbQuery = Product::query(); $dbQuery = $this->queryGraphSQL($dbQuery, $query, new Product); $products = $dbQuery->get(); return [ 'success' => true, 'data' => ['products' => $products] ]; } catch (\Exception $exception) { return [ 'success' => false, 'message' => $exception->getMessage() ]; } } . . . }
现在让我们检查使用查询参数得到的结果
API
http://127.0.0.1:8800/api/product/list
http://127.0.0.1:8800/api/product/list?graph={*}
返回相同的内容。
现在让我们尝试图形字符串并检查我们得到的结果
{*}
{name,image}
{id,name,image,_timestamps}
{name,image,category{*}}
{name,image,category{name}}
{name,image,category{name},variations{*}}
{name,image,category{name},variations.count}
{name,image,category{name},variations.sum.sale}
{name,image,category{name},variations(status=1).count}
- 你的想象力是无限的
同样尝试对 product/{id}
API 进行测试。
一些示例 API
// category
http://127.0.0.1:8800/api/category/list?graph={name,description,parent{name}}
http://127.0.0.1:8800/api/category/10?graph={name,description,products{name}}
// product
http://127.0.0.1:8800/api/product/list?graph={name,image,category{name}}
http://127.0.0.1:8800/api/product/2?graph={*,category{name},variations{*}}
// user profile
http://127.0.0.1:8800/api/profile?graph={name,email,phone,addresses{*}}
// cart items (cart_items table should have 'product_id', 'product_variation_id' columns)
http://127.0.0.1:8800/api/cart-item/list?graph={quantity,_timestamps,product{name,image},productVariation{*}}
附加方法
GraphSql 为传统查询提供了一些简写
http://127.0.0.1:8800/api/product/list?page=1&length=10&order_by=name,asc&status=1&category_id=1&brand=Hitachi,LG&graph={name,image,category{name}}
这里我们有了可选的 pagination
、status
和 category_id
列筛选、brand
列的多选项筛选、按任何列排序
让我们首先看看典型的实现
app/Http/Services/ProductService.php
use Bitsmind\GraphSql\QueryAssist;
class ProductService extends Service
{
use QueryAssist;
public function getList (array $query): array
{
try {
$dbQuery = Product::query();
// graphSql
$dbQuery = $this->queryGraphSQL($dbQuery, $query, new Product);
// sorting
if (array_key_exists('order_by', $query)) {
[$column, $order] = explode(',',$query['order_by']);
$dbQuery = $dbQuery->orderby($column, $order);
}
else {
// default
$dbQuery = $dbQuery->orderby('id', 'desc');
}
// column filters
if (array_key_exists('status', $query)) {
$dbQuery = $dbQuery->where('status', $query['status'])
}
if (array_key_exists('category_id', $query)) {
$dbQuery = $dbQuery->where('category_id', $query['category_id'])
}
// multi-options filters
if (array_key_exists('brand', $query)) {
$options = explode(',', $query[$field]);
$dbQuery = $dbQuery->whereIn('brand', $options);
}
// pagination
$count = $dbQuery->count();
if (!array_key_exists('page', $query)) $query['page'] = 1;
if (!array_key_exists('length', $query)) $query['length'] = 100;
$offset = ($query['page']-1)*$query['length'];
$products = $dbQuery->offset($offset)->limit($query['length'])->get();
return [
'success' => true,
'data' => [
'page' => $query['page'],
'length' => $query['length'],
'count' => $count,
'products' => $products
]
];
}
catch (\Exception $exception) {
return [
'success' => false,
'message' => $exception->getMessage()
];
}
}
}
GraphSql 简写
app/Http/Services/ProductService.php
use Bitsmind\GraphSql\QueryAssist;
class ProductService extends Service
{
use QueryAssist;
public function getList (array $query): array
{
try {
$dbQuery = Product::query();
$dbQuery = $this->queryGraphSQL($dbQuery, $query, new Product); // graphSql
$dbQuery = $this->queryOrderBy($dbQuery, $query, 'id', 'desc'); // sorting (default id,desc)
$dbQuery = $this->queryWhere($dbQuery, $query, ['status','category_id']); // column filters
$dbQuery = $this->queryWhereIn($dbQuery, $query, ['brand']); // multi-option filters
$count = $dbQuery->count();
$products = $this->queryPagination($dbQuery, $query)->get(); // pagination
return [
'success' => true,
'data' => [
'page' => $query['page'],
'length' => $query['length'],
'count' => $count,
'products' => $products
]
];
}
catch (\Exception $exception) {
return [
'success' => false,
'message' => $exception->getMessage()
];
}
}
}
太棒了!
注意
如果你有敏感数据,那么允许直接使用图形字符串是有风险的。
如何?
想象一个有身份验证的系统。因此,任何用户都不允许访问其他用户的数据。但是看看下面的 API 调用
http://127.0.0.1:8800/api/product/2?graph={*,orderItems{*,order{*,user{*}}}}
这个 API 将返回产品的数据,无论订单是来自这个用户还是其他用户。
那么解决方案是什么?
GraphSql 为此提供了现成的解决方案
解决方案 1:Graphsql 关键映射
解决方案 2:Graphsql 字符串加密
我们可以映射所有字符串,然后使用它们的映射键或使用加密字符串。
1. GraphSql 关键映射
还记得我们安装时迁移的 graph_sql_keys
表吗?我们将把我们的图形字符串保存到该表中,并分别为字符串设置一个键:customer_product_list
和 {name,image,category{name}}
分别在 key
和 string
列中。我们将在 API 中使用 graph_key
而不是 graph
查询参数。
表: graph_sql_keys
让我们设置图形键的 CRUD
- 在
routes/api.php
中创建路由。建议将 API 保持为私有。use App\Http\Controllers\ProductController; use Illuminate\Support\Facades\Route; Route::middleware('auth:api')->prefix('/graph-sql-key')->group(function () { Route::get('/list', [GraphSqlKeyController::class, 'getList']); Route::post('/sync', [GraphSqlKeyController::class, 'sync']); });
- 添加控制器
app/Http/Controllers/GraphSqlKeyController.php
namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Http\Services\ProductService; class GraphSqlKeyController extends Controller { function __construct (private readonly GraphSqlKeyService $service) {} public function getList (): JsonResponse { return response()->json( $this->service->getList()); } public function sync (GraphSqlKeySyncRequest $request): JsonResponse { return response()->json( $this->service->syncGraphSqlKey( $request->all())); } }
- 添加服务
app/Http/Services/GraphSqlKeyService.php
namespace App\Http\Services; use Bitsmind\GraphSql\Models\GraphSqlKey; class GraphSqlKeyService { public function getList(): array { try { $graphSqlKeys = GraphSqlKey::orderBy('key','asc')->get(); return [ 'success' => true, 'data' => ['graphSqlKeys' => $graphSqlKeys] ]; } catch (\Exception $exception) { return [ 'success' => false, 'message' => $exception->getMessage() ]; } } public function syncGraphSqlKey(array $data): array { try { $graphSqlKey = GraphSqlKey::where('key', $data['key'])->first(); if ($graphSqlKey) { $graphSqlKey->update([ 'string' => $data['string'] ]); } else { GraphSqlKey::create([ 'key' => $data['key'], 'string' => $data['string'] ]); } return [ 'success' => true, 'message' => 'GraphSql Key Synced Successfully' ]; } catch (\Exception $exception) { return [ 'success' => false, 'message' => $exception->getMessage() ]; } } }
使用 $this->queryGraphSQLByKey
而不是 $this->queryGraphSQL
。
现在 API 调用
http://127.0.0.1:8800/api/product/list?graph_key=customer_product_list
2. GraphSql 字符串加密
图形字符串可以被加密并发送为查询参数。记住,加密是昂贵的。
-
在前端使用此加密函数首先加密字符串
//js function encrypt (str, secret) { const refCharSet =',_.-=><*:!(){}[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' const refCharArray = refCharSet.split('') let shiftStrSet = secret.split('.') let encryptedStr = ''; // cipher for (let i = 0; i < str.length; i++) { let shift = shiftStrSet[0][i % shiftStrSet[0].length].charCodeAt(0); shift = shift >= 48 && shift <= 57 ? shift - 48 : shift % refCharSet.length let index = refCharArray.indexOf(str.charAt(i)) if (index > -1) { encryptedStr += refCharArray[(index + shift) % refCharSet.length]; } else { encryptedStr += str.charAt(i); } } for (let sss=1; sss<shiftStrSet.length; sss++) { //scramble let charArray = encryptedStr.split(''); for (let i = 0; i < encryptedStr.length; i++) { let shift = shiftStrSet[sss][i % shiftStrSet[sss].length].charCodeAt(0); let newIndex = shift >= 48 && shift <= 57 ? shift - 48 : shift % encryptedStr.length; [charArray[i], charArray[newIndex]] = [charArray[newIndex], charArray[i]]; } encryptedStr = charArray.join(''); } return encryptedStr }
-
在
.env
中设置一个密钥。密钥由由.
分隔的字母数字字符串组成。例如:cipher.scramble1st.scramble2nd.scramble3rd
第一部分生成字符串的密文,后续部分对密文进行混淆。建议使用两个部分的密钥。增加更多的部分将生成更复杂的加密。在前端加密时也使用该密钥GRAPHSQL_SECRET=Gxe44Ybneaexc74scescet3.DcYxw4a5
-
在
app/Http/Services/ProductService.php
中使用$this->queryGraphSQLEncrypted
而不是$this->queryGraphSQL
。
现在 API 调用
let graph = '{name,image,category{name}}'
let secret = 'Gxe44Ybneaexc74scescet3.DcYxw4a5'
let graphEnc = encrypt(graph, secret)
http://127.0.0.1:8800/api/product/list?graph_enc=${graphEnc}
对于开发环境,我们通常不需要加密。在这种情况下,我们可以使用 GRAPHSQL_SECRET=0
。然后加密字符串将与原始图形字符串相同。这有助于我们在开发环境中轻松调试。
使用键映射需要管理增删改查操作或手动更新表格。从CPU的角度来看,使用加密稍微有些昂贵。
因此,选择最适合您的一个。