bvisonl / sync-bundle
Symfony NTISyncBundle
Requires
- symfony/serializer: >=2.0
README
安装
-
使用Composer安装包
$ composer require nti/sync-bundle "dev-master"
-
将包配置添加到AppKernel
public function registerBundles() { $bundles = array( ... new NTI\SyncBundle\NTISyncBundle(), ... ); }
-
更新数据库模式
$ php app/console doctrine:schema:update
-
将路由添加到您的
routing.yml
... nti_sync: resource: "@NTISyncBundle/Resources/config/routing.yml"
需求
以下是需要考虑的事项列表,以便实现此包
- 在同步过程中需要考虑的任何实体都必须在类级别具有
@NTI\SyncEntity
注解。 - 应使用注解
@NTI\SyncParent(getter="[Getter Name]")
(请参阅下面的示例以获取更多信息)来更改父实体的最后同步时间戳的ManyToOne
关系。 - 要同步的实体必须有一个实现
SyncRepositoryInterface
的存储库(请参阅以下信息)。 - 对于每个实体,都需要配置映射
SyncMapping
,因为它用作查找的参考列表。 - 应为每个映射创建
SyncState
。这可以在创建所有SyncMapping
之后使用此查询完成。`INSERT INTO nti_sync_state(mapping_id, timestamp) SELECT id, 0 FROM sync_nti_mapping;`
- 如果实体将从客户端同步,则必须在
SyncMapping
数据库条目中定义一个服务。此外,此方法需要实现接口SyncServiceInterface
。
跟踪更改
该包跟踪同步更改的方式如下
- 该包有一个
DoctrineEventListener
,它监听onFlush
事件。 - 一旦事件被触发,该包将捕获具有
@NTI\SyncEntity
注解的每个实体。 - 如果实体具有定义的
SyncMapping
,则系统将此映射的last_timestamp
字段更新为当前的time()
。 - 如果实体有一个名为
setLastTimestamp()
的方法,它将使用time()
作为参数调用该方法,并重新计算或计算更改。 - 将检查实体的所有属性,以寻找包含注解
@NTI\SyncParent(getter="[Getter Name]")
的属性。如果找到,则调用getter,如果结果是具有@NTI\SyncEntity
的实体对象,则从点#3开始再次处理。此过程是递归的。
配置
以下是包的默认配置。如果您需要修改默认设置,请在您的config.yml
中进行修改。
nti_sync: deletes: # Identifier to use when an item gets deleted. This would go in your `deletes` section as shown below identifier_getter: "getId"
类示例
```
<?php
...
use NTI\Annotations as NTI;
/**
* ...
* @NTI\SyncEntity()
*/
public class Product {
...
/**
* @ORM\Column(name="last_timestamp", type="bigint", options={"default": 0})
*/
private $lastTimestamp;
...
/**
* Set lastTimestamp
* @param $lastTimestamp
* @return Company
*/
public function setLastTimestamp($lastTimestamp)
{
$this->lastTimestamp = $lastTimestamp;
return $this;
}
/**
* Get lastTimestamp
* @return integer
*/
public function getLastTimestamp()
{
return $this->lastTimestamp;
}
}
可以使用以下示例类使用ManyToOne
,其中子实体也需要更新父实体的last_timestamp
:
```
<?php
...
use NTI\Annotations as NTI;
/**
* ...
* @NTI\SyncEntity()
*/
public class ProductChild {
...
/**
* @NTI\SyncParent(getter="getProduct")
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\Product\Product")
*/
private $product;
...
/**
* Get Product
* @return Product
*/
public function getProduct()
{
return $this->product;
}
}
以下是包跟踪同步状态的通用过程:
以下是客户端在特定时间戳后请求更改时发生的一般过程:
实现(拉取)
同步过程的理念是,将要同步的每个对象都应该在它的存储库中实现SyncRepositoryInterface
。
/**
* Interface SyncRepositoryInterface
* @package NTI\SyncBundle\Interfaces
*/
interface SyncRepositoryInterface {
/**
* This function should return a plain array containing the results to be sent to the client
* when a sync is requested. The container is also passed as a parameter in order to give additional
* flexibility to the repository when making decision on what to show to the client. For example, if the user
* making the request only has access to a portion of the data, this can be handled via the container in this method
* of the repository.
*
* Note 1: If the `updatedOn` of a child entity is the one that is affected and not the parent, you may have to take that
* into account when doing your queries so that the updated information shows up in the results if desired when doing
* the comparison with the timestamp
*
* For example:
*
* $qb -> ...
* $qb -> leftJoin('a.b', 'b')
* $qb -> andWhere($qb->expr()->orX(
* $qb->expr()->gte('a.lastTimestamp', $date),
* $qb->expr()->gte('b.lastTimestamp', $date)
* ))
* ...
*
* This way if the only way of syncronizing B is through A, next time A gets synched B changes will be reflected.
*
* The resulting structure should be the following:
*
* array(
* "data" => (array of objects),
* SyncState::REAL_LAST_TIMESTAMP => (last updated_on date from the array of objects),
* )
*
*
* @param $timestamp
* @param ContainerInterface $container
* @param array $serializationGroups
* @return mixed
*/
public function findFromTimestamp($timestamp, ContainerInterface $container, $serializationGroups = array());
除了实现接口之外,在数据库的nti_sync_mapping
中,应配置将要同步的每个类的映射以及一个名称。
首先,想法是从服务器获取更改和映射的摘要
GET /nti/sync/summary
服务器将响应以下结构:
[
{
"id": 1,
"mapping": {
"id": 1,
"name": "Product",
"class": "AppBundle\\Entity\\Product\\Product",
"sync_service": "AppBundle\\Service\\Product\\ProductService"
},
"timestamp": 1515076764
},
...
]
响应包含一系列映射及其最后注册的时间戳。这个时间戳可以在同步过程中用来确定哪些内容已更改以及需要同步的内容。
然后,第三方使用以下结构向服务器发送请求
POST /nti/sync/pull
Content-Type: application/json
{
"mappings": [
{ "mapping": "[MAPPING_NAME]", "timestamp": [LAST_TIMESTAMP_CHECKED] }
]
}
在收到请求后,如果存在具有指定名称的映射,系统将调用存储库的findFromTimestamp实现并返回以下结果(以产品实体为例)
{
"[MAPPING_NAME]": {
"changes": [
{
"id": 2,
"productId": "POTATOBAG",
"name": "Potato bag",
"description": "Bag of potatoes",
"price": "32.99",
"cost": "0",
"createdOn": "11/30/2017 04:22:49 PM",
"updatedOn": "11/30/2017 04:22:49 PM",
"lastTimestamp": 1515068439
},
...
],
"newItems": [
{
"id": 1,
"uuid": "24a7aff0-fea8-4f62-b421-6f97f464f310",
"mapping": {
"id": 1,
"name": "Product",
"class": "AppBundle\\Entity\\Product\\Product",
"sync_service": "AppBundle\\Service\\Product\\ProductService"
},
"class_id": 8,
"timestamp": 1515068439
},
...
],
"deletes": [
{
"id": 2,
"mapping": {
"id": 2,
"name": "Product",
"class": "AppBundle\\Entity\\Product\\Product"
},
"classId": "[identifier_getter result]",
"timestamp": 1512080746
},
...
],
"failedItems": [
{
"id": 7,
"uuid": "abcdefg-123456-hifgxyz-78901",
"mapping": {
"id": 9,
"name": "Product",
"class": "AppBundle\\Entity\\Product\\Product",
"sync_service": " ... "
},
"classId": 137,
"timestamp": 1512080747,
"errors": [...errors provided...]
},
...
],
"_real_last_timestamp": 1512092445
}
}
服务器将返回changes
、newItems
、failedItems
和deletes
。其中,changes
将包含存储库SyncRepositoryInterface
实现返回的数组中的data
部分。deletes
将包含自指定时间戳以来记录的SyncDeleteState
列表。newItems
将包含SyncNewItemState
列表,表示自提供的时间戳以来创建的新项目,包括当时提供的UUID(这有助于第三方设备在首次拉取信息时验证项目是否已被创建,即使它们在本地存储中没有该项目的ID,也可以避免在服务器上创建重复项)。failedItems
将包含SyncFailedItemState
列表,列表中的每个项目都包含一个具有错误信息的errors
属性,这些错误信息是在处理实体的创建或更新时发现的。
_real_last_timestamp
应该被使用,因为它可以帮助对全同步的结果进行分页,并帮助客户端获取响应中最后一个对象的真正最后时间戳。这需要在存储库中获取,可以通过简单地从存储库的结果中获取最后一个项并调用getLastTimestamp()
来实现。
从现在开始,客户端必须跟踪_real_last_timestamp
以执行未来的同步。
实现(推送)
以下是推送/拉取过程的一般思路
服务器端
对于每个映射的实体SyncMapping
,应该指定一个服务。该服务必须实现SyncServiceInterface
。
客户端
为了处理第三方设备的推送,它必须在请求中提供以下结构
POST /nti/sync/push
Content-Type: application/json
{
"mappings": [
{ "mapping": "[MAPPING_NAME]", "data": [
{
"id": "5eb86d4a-9b82-42f3-abae-82b1b61ad58e",
"serverId": 1,
"name": "Product1",
"description": "Description of the product",
"price": 55,
"lastTimestamp": 1512080746
},
...
] }
]
}
当服务器收到此请求时,它将在相应的SyncMapping
中执行配置的服务sync()
方法,并将该参数的数据数组传递给它。您的sync()
函数需要操作这些信息,并返回一个数组,该数组将包含在响应中相应映射名称下。
然后服务器返回以下结构
{
"mappings": [
{ "[MAPPING_NAME]": "RESULT OF YOUR sync() HERE" },
{ "[MAPPING_NAME]": "RESULT OF YOUR sync() HERE" },
{ "[MAPPING_NAME]": "RESULT OF YOUR sync() HERE" },
]
}
待办事项
- 处理来自第三方的删除
ManyToMany
关系很复杂,可能导致性能问题