tomwalder/php-gds

PHP的Google Cloud Datastore库。也支持在Datastore模式下使用Firestore。

v6.1.0 2023-12-01 08:14 UTC

README

Build Status Coverage Status

PHP的Google Cloud Datastore库

Google Cloud Datastore 是一个优秀的NoSQL解决方案(托管、可扩展、免费到一定程度),但将其在PHP中实现数据持久性的“Hello World”可能有点棘手(例如,需要大量的代码粘合剂)。

此库旨在使您更轻松地开始使用并使用应用程序中的Datastore。

快速入门

composer require "tomwalder/php-gds:^6.1"
// Build a new entity
$obj_book = new GDS\Entity();
$obj_book->title = 'Romeo and Juliet';
$obj_book->author = 'William Shakespeare';
$obj_book->isbn = '1840224339';

// Write it to Datastore
$obj_store = new GDS\Store('Book');
$obj_store->upsert($obj_book);

// Fetch all books
foreach($obj_store->fetchAll() as $obj_book) {
    echo "Title: {$obj_book->title}, ISBN: {$obj_book->isbn} <br />", PHP_EOL;
}

新功能 6.1版本

对某些类型的错误支持自动指数退避。请在此处查看文档:https://cloud.google.com/datastore/docs/concepts/errors

启用方法

\GDS\Gateway::exponentialBackoff(true);

版本 6.0.0 引入了更好的(但不同的)对 NULL 值的支持。

新功能 5.0版本

自版本 5(2021年5月)起,此库提供以下支持

  • PHP 7 第二代App Engine运行时 - 默认使用REST API
  • PHP 7 "任何地方"(例如Google Compute Engine、Cloud Run、GKE) - 使用REST或gRPC

自版本 5起删除的关键功能

  • PHP 5支持
  • 对第一代App Engine运行时中内置的旧版“协议缓冲区”API的支持

如果您需要在那个基础设施上继续运行应用程序,请坚持使用4.x或更早的版本。

目录

使用REST API(自5.0版默认)

自PHP-GDS版本5起,REST API网关是默认的。

它将尝试自动检测您的Google项目ID - 通常Google身份验证库将使用默认的应用程序凭据。

您可能需要首先设置一个环境变量,指定您的JSON凭证文件的路径,通常如果您在App Engine或Google Compute Engine之外运行。

putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json');

// A regular Store, but with a custom Gateway
$obj_book_store = new GDS\Store('Book', new \GDS\Gateway\RESTv1('my-project-id'));

您可以在以下位置了解更多关于身份验证系统: PHP身份验证库

您可以从Google Cloud Console的 API Manager > Credentials 下载服务帐户JSON文件。

在Datastore模式下使用Firestore

如果您正在使用PHP-GDS版本4或更早版本,并且从App Engine标准(第一代)使用Datastore模式的Firestore,您可能会遇到来自 Protobuf 网关的 内部错误

您可以通过使用 RESTv1 网关和API来解决此问题。 请在此处查看基本指导

更多详细信息和推荐的升级路径将陆续发布。同时,还将提供更好的gRPC支持,适用于App Engine之外的环境。

示例

我发现示例是决定是否尝试使用库的绝佳方式,所以这里有一些供您参考。

// Build a new entity
$obj_book = new GDS\Entity();
$obj_book->title = 'Romeo and Juliet';
$obj_book->author = 'William Shakespeare';
$obj_book->isbn = '1840224339';

// Write it to Datastore
$obj_store = new GDS\Store('Book');
$obj_store->upsert($obj_book);

您还可以使用替代数组语法来创建实体对象,例如这样

$obj_book = $obj_store->createEntity([
    'title' => 'The Merchant of Venice',
    'author' => 'William Shakespeare',
    'isbn' => '1840224312'
]);

现在让我们从Datastore中获取所有书籍,并显示它们的标题和ISBN号码

$obj_store = new GDS\Store('Book');
foreach($obj_store->fetchAll() as $obj_book) {
    echo "Title: {$obj_book->title}, ISBN: {$obj_book->isbn} <br />", PHP_EOL;
}

关于示例的更多信息

这些初始示例假设您正在运行Google AppEngine应用程序或在本地AppEngine开发环境中。在这两种情况下,我们可以自动检测数据集

我们使用GDS\Store来读取和写入Datastore中的GDS\Entity对象。

这些示例使用具有动态Schema的通用GDS\Entity类。有关自定义Schema和索引字段的更多详细信息,请参阅下方的定义您的模型

演示应用程序

一个简单的留言簿应用程序

应用程序:http://php-gds-demo.appspot.com/

代码:https://github.com/tomwalder/php-gds-demo

版本5中的更改

  • 添加PHP 7支持
  • 删除PHP 5支持
  • 删除App Engine第一代运行时支持(包括直接Protocol Buffer API)

更新时区支持

在5.1中,改进了DateTime对象进出Datastore的时区支持。

数据是如何存储的

Datastore将数据记录为UTC。当您在Google Cloud Console中浏览数据时,它们将代表您的本地时区。

通过PHP-GDS作为实体输出的数据

现在您应该期待从PHP-GDS输出的Datastore中的任何DateTime对象都应用了您当前的PHP默认时区。以下是一个示例

date_default_timezone_set('America/New_York');

$obj_store = new GDS\Store('Book');
$obj_book = $obj_store->fetchOne();
echo $obj_book->published->format('c'); // 2004-02-12T15:19:21-05:00
echo $obj_book->published->getTimezone()->getName(); // America/New_York

输入数据 - 多格式支持

如果您传递一个DateTime对象(或任何匹配DateTimeInterface的对象),我们将尊重它上设置的时区。

传递给datetime字段的任何其他基于字符串的值在转换为UTC之前将转换为DateTimeImmutable对象,使用标准PHP方法:[https://php.ac.cn/manual/en/datetime.construct.php](https://php.ac.cn/manual/en/datetime.construct.php)

这意味着除非使用时间戳值(例如@946684800),或带有已声明时区的值(例如2010-01-28T15:00:00+02:00),否则我们将假设该值位于您的当前时区上下文中。

版本4中的更改

  • 更一致地使用DateTime对象 - 现在所有结果集都将使用它们而不是Y-m-d H:i:s字符串
  • google/auth移动到可选依赖项 - 如果您需要REST API

版本3中的更改

  • 支持新的Datastore API,v1 - 通过REST
  • 删除对旧1.x系列“PHP Google API Client”的支持
  • GeoPoint数据现在也通过REST API v1以及ProtoBuf支持

入门

您坐得舒服吗?在我们开始之前,您需要

  • 一个Google账户(当然),通常用于运行AppEngine - 但不总是这样
  • 一个开启“Google Cloud Datastore API”的项目来工作 Google开发者控制台

如果您想从远程或非App Engine环境中使用JSON API,您还需要

  • 应用程序默认凭据
  • 一个“服务账户”和从开发者控制台下载的JSON服务密钥文件

Composer,依赖项

使用Composer安装

composer require "tomwalder/php-gds:^5.1"

与Datastore模拟器一起使用

本地开发支持使用REST网关和数据存储模拟器。

详细信息请在此处查看:[https://cloud.google.com/datastore/docs/tools/datastore-emulator](https://cloud.google.com/datastore/docs/tools/datastore-emulator)

定义您的模型

因为数据存储是无模式的,库也支持那些未显式定义的字段/属性。但通常在开始时就定义实体模式是有很多意义的。

以下是构建我们的示例 Schema 的方法,使用数据存储实体类型为 "Book" 和 3 个字段。

$obj_schema = (new GDS\Schema('Book'))
   ->addString('title')
   ->addString('author')
   ->addString('isbn');
   
// The Store accepts a Schema object or Kind name as its first parameter
$obj_book_store = new GDS\Store($obj_schema);

默认情况下,所有字段都是索引的。索引字段可以用于 WHERE 子句。您可以通过将 FALSE 作为 addString() 的第二个参数传递来显式配置一个字段不被索引。

如果您使用动态模式(即不定义,只是使用实体名称),则该记录的所有字段都将被索引。

可用的 Schema 配置方法

  • GDS\Schema::addString
  • GDS\Schema::addInteger
  • GDS\Schema::addDatetime
  • GDS\Schema::addFloat
  • GDS\Schema::addBoolean
  • GDS\Schema::addStringList
  • GDS\Schema::addGeopoint

请查看 examples 文件夹以获取一组完整操作的代码。

创建记录

替代数组语法

除了直接构建一个新的 GDS\Entity 并设置其成员数据外,还有另一种方法,即使用 GDS\Store::createEntity 工厂方法,如下所示。

$obj_book = $obj_book_store->createEntity([
    'title' => 'The Merchant of Venice',
    'author' => 'William Shakespeare',
    'isbn' => '1840224312'
]);

特殊属性

除了标量值外,还支持两种 "对象" 数据类型

DateTime

支持 DateTime 对象绑定(也请参见下文查询参数绑定)

$obj_book = $obj_book_store->createEntity([
    'title' => 'Some Book',
    'author' => 'A N Other Guy',
    'isbn' => '1840224313',
    'published' => new DateTime('-5 years')
]);

Geopoint

该库最近添加了对 Geopoint 属性的支持。

$obj_schema->addGeopoint('location');

设置数据时,使用 Geopoint 对象

$obj_person->location = new GDS\Property\Geopoint(53.4723272, -2.2936314);

当从结果中提取 geopoint 数据时

echo $obj_person->location->getLatitude();
echo $obj_person->location->getLongitude();

目前无法查询 Geopoint 字段,尽管该功能与 Google 合作处于 Alpha 阶段

查询、GQL和默认查询

GDS\Store 对象使用 Datastore GQL 作为其查询语言。以下是一个示例

$obj_book_store->fetchOne("SELECT * FROM Book WHERE isbn = '1853260304'");

并支持命名参数绑定(字符串、整数)(建议使用此方法

$obj_book_store->fetchOne("SELECT * FROM Book WHERE isbn = @isbnNumber", [
   'isbnNumber' => '1853260304'
]);

支持 DateTime 对象绑定

$obj_book_store->fetchOne("SELECT * FROM Task WHERE date_date < @now", [
   'now' => new DateTime()
]);

我们提供了一些辅助方法来执行一些常见的(根实体)查询,单个和批量(比许多单独的获取调用更有效)

  • GDS\Store::fetchById
  • GDS\Store::fetchByIds - 批量获取
  • GDS\Store::fetchByName
  • GDS\Store::fetchByNames - 批量获取

当您实例化一个存储对象,如我们的示例中的 BookStore 时,它将预先加载以下形式的默认 GQL 查询(这是 "默认查询")

SELECT * FROM <Kind> ORDER BY __key__ ASC

这意味着您可以快速轻松地获取一个或多个记录,而无需编写任何 GQL,如下所示

$obj_store->fetchOne();     // Gets the first book
$obj_store->fetchAll();     // Gets all books
$obj_store->fetchPage(10);  // Gets the first 10 books

1000 结果批量限制

默认情况下,此库将包含 1,000 条记录的 "批量大小"。

这意味着调用 fetchAll() 只会返回 1,000 条记录。

如果您需要超过 1,000 条记录,请建议使用 fetchPage() 进行分页。

本地开发服务器上的 GQL

在编写本指南时,Google App Engine 的本地开发服务器不支持 GQL。因此,从 2.0 版本起,我包含了一个基本的 GQL 解析器,该解析器仅在本地开发环境中使用,这意味着您可以在本地运行大多数应用程序场景,就像在生产环境中一样。

应将 GQL 解析器视为一个 "娱乐" 工具,而不是一个生产就绪的服务。

非常欢迎反馈 - 如果您有无法运行的 GQL 查询,只需提出问题,我会看看我能做什么(或分支 & PR!)

分页

在处理大型数据集时,分批查看结果可能很有用。以下是一个示例,通过 50 的倍数遍历所有书籍。

$obj_book_store->query('SELECT * FROM Book');
while($arr_page = $obj_book_store->fetchPage(50)) {
    echo "Page contains ", count($arr_page), " records", PHP_EOL;
}

限制、偏移量和光标

在标准的 SQL 环境中,上述分页将类似于以下内容

  • 对于第一页:SELECT * FROM Book LIMIT 0, 50
  • 对于第二页:SELECT * FROM Book LIMIT 50, 50,依此类推。

尽管您可以使用与Datastore GQL非常相似的语法,但这可能是不必要的成本。这是因为查询运行时扫描的每一行都会收费。因此,执行相当于LIMIT 5000, 50的操作将计为5,050次读取 - 而不仅仅是实际返回的50次。

使用游标可以解决这个问题。所有实现都封装在GDS\Gateway类中,因此您无需担心。

底线:内置的分页尽可能使用游标,以实现最快和最经济的操作。

LIMIT读取操作技巧

调用GDS\Store::fetchOne时不要提供LIMIT子句 - 这是由您完成的(我们添加LIMIT 1

  • GDS\Store::fetchPage - 同样,这是为您完成的,并可能引起冲突。
  • 价格与游标引用

查询游标

多租户应用程序和数据命名空间

通常,这适用于多租户应用程序,其中每个客户都会有自己的数据,即使在相同的“类型”中也是如此。

此库支持命名空间,并且可以通过传递可选的第三个命名空间参数来为每个Gateway实例进行配置。

通过配置命名空间,所有通过配置了命名空间的Gateway执行的操作都是在该命名空间上下文中执行的。当执行upsert/delete/fetch-by-key操作时,命名空间会自动应用于键,当运行GQL查询时,会应用于请求。

更详细的示例包含在示例文件夹中。

// Create a store for a particular customer or 'application namespace'
$obj_gateway = new \GDS\Gateway\RESTv1('project-id', 'namespace');
$obj_store = new \GDS\Store('Book', $obj_gateway);

Google数据存储允许(并鼓励)实体以分层组织。

实体组、层次结构和祖先

这种分层允许一定量的“关系”数据。例如,一个ForumThread实体可能有一个或多个作为子实体的ForumPosts实体。

实体组是一个非常高级的主题,但可以在多个领域对您的应用程序产生积极影响,包括

事务完整性

  • 强一致性数据
  • 在撰写本文时,我支持通过以下方法与实体组一起工作

GDS\Entity::setAncestry

  • GDS\Entity::getAncestry
  • GDS\Store::fetchEntityGroup
  • GDS\Store支持在事务中运行更新和删除。

事务

要开始事务

然后,任何更改数据的操作都将提交并消耗事务。因此,对另一个操作的即时调用将不会是事务性的。

$obj_store->beginTransaction();

注意GDS\Exception\Contention异常 - 如果您在本地开发或通过实时网关意外触发了数据存储的竞争条件,库应该抛出这些异常。

// Data changed within a transaction
$obj_store->upsert($obj_entity);

// Not transactional
$obj_store->delete($obj_entity);

自定义实体类和存储

虽然您可以直接使用上述示例中的GDS\EntityGDS\Store类,但您可能发现扩展一个或另一个类很有用。

例如

这样,当您从数据存储中提取对象时,它们是您定义的实体类的对象。

class Book extends GDS\Entity { /* ... */ }
$obj_store->setEntityClass('\\Book');

Schema包含自定义实体类名称 - 这可以直接设置,或通过Store对象设置。

重新索引

当您将字段从非索引更改为索引时,您需要在数据存储返回针对该索引运行查询之前“重新索引”所有现有实体。这是由于谷歌更新其BigTable索引的方式。

我已在示例文件夹中包含了一个简单的示例(分页)重新索引脚本,reindex.php

使用多个网关类,您可以在命名空间之间

数据迁移

以及在本地和实时环境之间移动数据。

// Name-spaced Gateways
$obj_gateway_one = new \GDS\Gateway\RESTv1('project-id', 'namespace_one');
$obj_gateway_two = new \GDS\Gateway\RESTv1('project-id', 'namespace_two');

// Grab some books from one
$arr_books = (new \GDS\Store('Book', $obj_gateway_one))->fetchPage(20);

// And insert to two
(new \GDS\Store('Book', $obj_gateway_two))->upsert($arr_books);

注意:在这两个示例中,实体将以相同的KeyID或KeyNam

// Local and Remote Gateways
$obj_gateway_local = new \GDS\Gateway\ProtoBuf();
$obj_gateway_remote = new \GDS\Gateway\RESTv1('project-name');

// Grab some books from local
$arr_books = (new \GDS\Store('Book', $obj_gateway_local))->fetchPage(20);

// And insert to remote
(new \GDS\Store('Book', $obj_gateway_remote))->upsert($arr_books);

插入

关于Google Cloud Datastore的更多信息

谷歌的说法

使用托管的无模式NoSQL数据库来存储非关系型数据。Cloud Datastore可以自动根据需要扩展,并支持事务以及强大的类似SQL查询。

https://cloud.google.com/datastore/

具体主题

一些你可能想阅读的突出主题

单元测试

正在进行一套完整的单元测试。假设你已经使用Composer安装了php-gds及其依赖项,你可以运行

vendor/bin/phpunit

或者,如果你需要运行容器化测试,你可以使用runphp镜像(或你选择的任何镜像)

docker run --rm -it -v`pwd`:/app -w /app fluentthinking/runphp:7.4.33-v0.9.0 php /app/vendor/bin/phpunit

点击此处获取更多详细信息.

脚注

我当然更熟悉SQL和关系型数据模型,所以我认为这可能会在代码中体现出来——无论是正确还是不正确!

感谢@sjlangley提供的所有输入——特别是关于Protocol Buffers的单元测试。

虽然我在生产中使用了这个库,但我希望其他人也能从中受益。欢迎反馈。

其他App Engine软件

如果你喜欢这个,你可能对我为PHP在Google App Engine上的全文搜索引擎库感兴趣