发现存储特定 object-oriented 数据结构的最佳方法
Discovering the best approach to storing a specific object-oriented data structure
经过一些绝妙的建议,以及由于可能最终解决我的问题而兴奋的不眠之夜,我意识到我仍然没有找到解决方案。所以,我在这里更详细地概述我的问题,希望有人知道实现这一目标的最佳方法。
回顾一下(如果你还没有读过):
- 我正在从头构建一个PHP OOP框架(我在这件事上别无选择)
- 框架需要以最有效的方式处理 object-oriented 数据。它不需要快如闪电,只需要是问题的最佳解决方案
- Objects 非常类似于严格编写的 oop objects,因为它们是特定 class 的实例,其中包含一组严格的属性。
- Object 属性可以是基本类型(字符串、数字、布尔值),但也可以是一个 object 实例,或 object 的数组(限制是数组必须是object个相同类型的)
最终,支持 document-oriented 存储的存储引擎(类似于 XML 或 JSON),其中 object 本身具有严格的结构。
我不会概述到目前为止我尝试过的内容(我在之前的 post 中简要讨论过)我将在本文的其余部分 post 详细解释它是什么我正在尝试这样做。 post 会很长(抱歉!)。
首先,我需要讨论一个术语,我必须引入该术语才能解决需求集带来的最关键问题之一。我将此术语命名为 "persistence"。我知道这个术语在处理 object 数据库时确实有不同的含义,因此我愿意接受关于不同术语的建议。但现在,让我们继续吧。
坚持
持久性是指object的独立性。在考虑从 XML 生成的数据结构时,我发现有必要引入这个术语(这是我必须能够做的事情)。在 XML 中,我们看到 object 完全依赖于它们的 parent,同时我们也看到 object 可以独立于 parent object.
以下示例是 XML 文档的示例,它符合特定结构(例如,.wsdl 文件)。每个 object 都类似于具有严格结构的类型。 每个object都有一个"id"属性
在上面的例子中,我们看到了两个用户。两者在 "address" 属性 下都有自己的地址 object。但是,如果我们查看它们的 "favouriteBook" 属性,我们可以看到它们都是 re-use 相同的 Book object 实例。另请注意,这些书使用同一作者。
所以我们的地址 object 是 non-persistent 因为它只与它的 parent object (用户)意味着它的实例只需要在拥有用户 object 存在时存在。然后 Book object 是 persistent 因为它可以在多个位置使用并且它的实例保持持久。
起初,我觉得想出这样一个术语有点疯狂,但是,我发现它非常容易理解和实际使用。它最终将 "many-to-many, one-to-many, one-to-one, many-to-one" 公式浓缩为一个简单的想法,我认为它在嵌套数据中效果更好。
我在这里对上述数据进行了图像表示:
根据我对持久性的定义,有一组规则可以帮助理解它。这些规则如下:
- update/create
- 持久化 child object 正在存储基础 object 更新 持久化 object 的属性,最终更新其实例。
- Non-persistent objects 总是 创建一个新的 object 实例以确保它们始终使用 non-persistent实例(在任何给定时间没有两个 non-persistent 个实例存在于超过一个地方)
- 正在删除
- 持久性 child object 基 object 不会被递归删除。这是因为持久化 object 可能存在于其他地方。您总是会直接删除持久性 object。
- Non-persistent child object 基数 object 与基数 object 一起被删除。如果不将它们移除,它们将被搁置,因为它们的设计要求具有 parent.
- 正在检索
- 由于持久性主要定义了修改的工作方式,因此检索与持久性关系不大,除了您期望持久性如何影响模型的存储方式以及模型的检索方式(持久性object 实例无论位于何处都保持持久性,non-persistent object 总是有自己的实例)
在我们继续之前要注意的最后一件事 - 数据模型的持久性由模型本身定义 而不是关系 。最初,持久性是关系的一部分,但当系统期望您了解模型的结构以及它们的使用方式时,这是完全没有必要的。最终,模型的每个模型实例要么是持久的,要么不是。
所以现在看看一些代码,您可能会开始看到疯狂背后的方法。虽然看起来这个解决方案的原因是能够围绕 objective 符合条件的数据构建一个存储系统,但它的设计实际上来自于希望能够存储 class 个实例, and/or 从 objective 数据结构生成 class 个实例。
我已经写了一些 pseudo-classes 作为我试图产生的功能的例子。我已经评论了大多数方法,包括类型声明。
因此,首先,这将是所有模型 class 扩展的基础 class。 class 的目的是在模型 class/object 和 database/storage 引擎之间创建一个层:
<?php
/**
* This is the base class that all models would extend. It contains the functionalities that are useful among all model
* objects, such as crud actions, finding, and crud event management.
*
* @author Donny Sutherland <donny@pixelpug.co.uk>
* @package Main
* @subpackage Sub
*
* Class ORMModel
*/
class ORMModel {
/**
* In order to generate relationships between objects, every object MUST have an id. This functions as the object's
* unique identifier. Each object in it's model type (collection) has it's own id.
*
* @var int
*/
public $id;
/**
* Internal property assigned by the application. This is where the persistence of the model is defined.
*
* @var bool
*/
protected $internal_isPersistent = true;
/**
* Internal property assigned by the application. This is an array of the model's properties, and their PHP type.
*
* For example, a User model might use something like this:
* array(
"id" => "integer",
* "username" => "string",
* "password" => "string",
* "address" => "object",
* "favouriteBook" => "object",
* "allBooks" => "array"
* )
*
* @var array
*/
protected $internal_propertyTypes = array();
/**
* Internal property assigned by the application. This is an array of the model's properties which are objects, and
* the MODEL CLASS type of the object.
*
* For example, the User model example for the property types might use this:
* array(
* "address" => "Address",
"favouriteBook" => "Book",
* "allBooks" => "Book"
* )
*
* @var array
*/
protected $internal_objectTypes = array();
/**
* I am not 100% sure on the best way to use this yet, I have tried a few different ways and all seem to cause
* performance problems. But ultimately, before we attempt to update an object, we cache it's currently stored
* instance to this property, allowing us to compare old vs new. I find this really useful for detecting whether a
* property has changed, I just need to work out the best way to do it.
*
* @var $this
*/
protected $internal_old;
/**
* The lazy way to construct an empty model object (all NULL values)
*
* @return $this
*/
final public static function constructEmpty() {
}
/**
* This method is used by the other constructFromXXX methods once the data has been converted to a PHP array.
* This method is what allows us to build a RESTful interface into the ORM system as it conforms to the following
* rules:
*
* - if the id is set (not null), first pull the object from storage.
* - For each key => value of the passed array, OVERWRITE the value
* - For properties that are model objects/arrays, if the property is assiged to the array:
* - if the array value is NULL, we are clearing the object relationship
* - if the array valus is not null, construct recursively at this point
*
* Ultimately, if you assign a property in the array that you pass to this method, it will overwrite the value. If
* you do not, it will use the property value in storage.
*
* @param array $array
*
* @return $this
*/
final public static function constructFromArray(array $array) {
}
/**
* This method attempts to decode the value of $json into a PHP array. It then calls constructFromArray if the string
* could be decoded.
*
* @param $json
*
* @return $this
*/
final public static function constructFromJson($json) {
}
/**
* This method attempts to decode the value of $xml into a PHP array. It then calls constructFromArray if the xml
* could be decoded.
*
* @param $xml
*
* @return $this
*/
final public static function constructFromXml($xml) {
}
/**
* Find one object, based on a set of options.
*
* @param ORMCrudOptions $options
*
* @return $this
*/
final public static function findOne(ORMCrudOptions $options) {
}
/**
* Find all objects, (optionally) based on a set of options
*
* @param ORMCrudOptions $options
*
* @return $this[]
*/
final public static function findAll(ORMCrudOptions $options=null) {
}
/**
* Find the count of objects, based on a set of optoins
*
* @param ORMCrudOptions $options
*
* @return integer
*/
final public static function findCount(ORMCrudOptions $options) {
}
/**
* Find one object, based on it's id, and (optionally) a set of options.
*
* @param ORMCrudOptions $options
*
* @return $this
*/
final public static function findById($id,ORMCrudOptions $options=null) {
}
/**
* Push this object to storage. This creates/updates all of the contained objects, based on their id's and
* persistence.
*
* @param ORMCrudOptions $options
*
* @return bool
*/
final public function pushThis(ORMCrudOptions $options) {
}
/**
* Pull this object form storage. This retrieves all of the contained objects again, based on their id's and
* persistence.
*
* @param ORMCrudOptions $options
*
* @return bool
*/
final public function pullThis(ORMCrudOptions $options) {
}
/**
* Remove this object from storage. This conditionally removes the contained objects (based on persistence) based
* on their id's.
*
* @param ORMCrudOptions $options
*/
final public function removeThis(ORMCrudOptions $options) {
}
/**
* This is a crud event.
*/
public function beforeCreate() {
}
/**
* This is a crud event.
*/
public function afterCreate() {
}
/**
* This is a crud event.
*/
public function beforeUpdate() {
}
/**
* This is a crud event.
*/
public function afterUpdate() {
}
/**
* This is a crud event.
*/
public function beforeRemove() {
}
/**
* This is a crud event.
*/
public function afterRemove() {
}
/**
* This is a crud event.
*/
public function beforeRetrieve() {
}
/**
* This is a crud event.
*/
public function afterRetrieve() {
}
}
因此,最终,此 class 旨在提供构建、查找、保存、检索和删除模型 object 的功能。内部属性是仅存在于 classes 中(不在存储中)的属性。当您使用界面创建模型并将 property/fields 添加到模型时,这些属性由框架本身填充。
想法是,该框架带有一个用于管理数据模型的界面。使用此界面,您可以创建模型,并将 property/fields 添加到模型中。这样做时,系统会自动为您创建 class 文件,并在您修改持久性和 属性 类型时更新这些内部属性。
为了让开发人员更友好,系统为每个模型创建了两个 class 文件。一个基础 class(扩展了 ORMModel)和另一个 class(扩展了基础 class)。基数 class 由系统操纵,因此不建议修改此文件。另一个 class 被开发人员用来为模型和 crud 事件添加额外的功能。
回到示例数据,这里是用户群 class:
<?php
class User_Base extends ORMModel {
public $name;
public $pass;
/**
* @var Address
*/
public $address;
/**
* @var Book
*/
public $favouriteBook;
protected $internal_isPersistent = true;
protected $internal_propertyTypes = array(
"id" => "integer",
"name" => "string",
"pass" => "string",
"address" => "object",
"favouriteBook" => "object"
);
protected $internal_objectTypes = array(
"address" => "Address",
"favouriteBook" => "Book"
);
}
几乎不言自明。再次注意,内部属性是由系统生成的,因此这些数组将根据您在模型管理界面中 creating/modifying User 模型时指定的 property/fields 生成。还要注意地址上的文档块和 favouriteBook
属性 定义。这些也是由系统生成的,使 classes 非常 IDE 友好。
这将是为用户模型生成的另一个 class:
<?php
final class User extends User_Base {
public function beforeCreate() {
}
public function afterCreate() {
}
public function beforeUpdate() {
}
public function afterUpdate() {
}
public function beforeRemove() {
}
public function afterRemove() {
}
public function beforeRetrieve() {
}
public function afterRetrieve() {
}
}
再一次,非常不言自明。我们扩展了基础 class 以创建另一个 class,开发人员可以在其中添加其他方法,并向 crud 事件添加功能。
我不会添加构成其余示例数据的其他 object。由于上面应该解释了它们的外观。
所以你 may/may 没有注意到在 ORMModel class 中,CRUD 方法需要一个 ORMCrudOptions class 的实例。这个 class 对整个系统来说非常重要,所以让我们快速看一下:
<?php
/**
* Despite this object being some-what aggregate, it it quite possibly the most important part of the ORM, in that it
* defines how CRUD actions are executed, and outline how the querying is done.
*
* Class ORMCrudOptions
*/
final class ORMCrudOptions {
/**
* This ultimately makes up the "where" part of the sql query. However, because we want to be able to make querying
* possible at any depth within the hierarchy of a model, this gets quite complicated.
*
* Previously, I developed a system which allowed the user to do something like this:
*
* "this.customer.address.postcode LIKE ('%XXX%') OR this.customer.address.line1 LIKE ('%XXX%')
*
* he "this" and the "." are my extension to basic sql. The "this" refers to the base model that you are finding,
* and each "." basically drills down into the hierarchy to make a comparison on a property somewhere within a
* contained model object.
*
* I will explain more how I did this in my post, I am most definitely looking at how I could better achieve this
* though.
*
* @var string
*/
private $query;
/**
* This allows you to build up a list of order by definitions.
*
* Using the orderBy method, you can chain up the order by statements like:
*
* ->orderBy("this.name","asc")->orderBy("this.customer.address.line1","desc")
*
* Which would be similar to doing:
*
* ORDER BY this_name ASC, this_customer_address.line1 DESC
*
* @var array
*/
private $orderBy;
/**
* This allows you to set the limit start and limit values by doing:
*
* ->limit(10,10)
*
* Which would be similar to doing:
*
* LIMIT 10, 10
*
* @var
*/
private $limit;
/**
* Depth was added in my later en devours to try and help with performance. It allows you to specify the depth at
* which to retrieve data. Although this helped with optimisation a lot, I really disliked having to use
* implement this because it seems like a work-around. I would rather be able to increase performance elsewhere so
* that objects are always retrieved at their full depth
*
* @var integer
*/
private $depth;
/**
* This was another newly added feature. Whenever you execute a crud action on a model, the model instance is stored
* in a local cache if this is true, and/or retrieved from this cached if this value is true.
*
* I did find this to make a significant increase on performance, although it did bring in complications that make
* the system tricky to use at times. You really need to understand how and when to use the cache, otherwise it can
* be infuriatingly obtuse.
*
* @var bool
*/
private $useCache;
/**
* Built into the ORM system, and tied in with the application I set up a webhook system which fires out webhooks on
* crud events. I discovered the need to be able to disable webhooks at times (when doing large amounts of crud
* actions in one go) pretty early on. Setting this to false basically disables webhooks on the crud action
*
* @var bool
*/
private $fireWebhooks;
/**
* Also build into the application, and tied into the ORM system is an access system. This works on a seperate
* layer to the database, allowing me to use the same access system as I use for everything in the framework as I do
* for defining crud action access. However, in some instances I found it useful to disable access checks.
*
* This is always on by default. In the api system that I built to access the data models, you were not able to
* modify this property and therefore were always subject to access checks.
*
* @var
*/
private $ignoreAccessChecks;
/**
* The lazy way to create a new instance of options.
*
* @return ORMCrudOptions
*/
public static function n() {
return new ORMCrudOptions();
}
/**
* Set the query value
*
* @param $query
*
* @return $this
*/
public function query($query) {
$this->query = $query;
return $this;
}
/**
* Add an orderby field and direction
*
* @param $field
* @param string $direction
*
* @return $this
* @internal param array $orderBy
*
*/
public function orderBy($field,$direction="asc") {
$this->orderBy[] = array($field,$direction);
return $this;
}
/**
* Set the limit start and limit.
*
* @param $limitResults
* @param null $limitStart
*
* @return $this
*/
public function limit($limitResults,$limitStart=null) {
$this->limit = array($limitResults,$limitStart);
return $this;
}
/**
* Set the depth for retrieval
*
* @param $depth
*
* @return $this
*/
public function depth($depth) {
$this->depth = $depth;
return $this;
}
/**
* Set whether to use the model cache
*
* @param $useCache
*
* @return $this
*/
public function useCache($useCache) {
$this->useCache = $useCache;
return $this;
}
/**
* Set whether to fire webhooks on crud actions
*
* @param $fireWebhooks
*
* @return $this
*/
public function fireWebhooks($fireWebhooks) {
$this->fireWebhooks = $fireWebhooks;
return $this;
}
/**
* Set whether to ignore access checks
*
* @param $ignoreAccessChecks
*
* @return $this
*/
public function ignoreAccessChecks($ignoreAccessChecks) {
$this->ignoreAccessChecks = $ignoreAccessChecks;
return $this;
}
}
这个 class 背后的想法是消除在 crud 方法中使用大量参数的需要,因为这些参数中的大多数可以 re-used 在所有粗糙的方法。记下对查询 属性 的评论,因为那很重要。
所以,这几乎涵盖了基础 psuedo-code 和我正在尝试做的事情背后的想法。所以最后,我会展示一些 user-scenarios:
<?php
//the most simple way to store a user
$user = User::constructEmpty();
//we use auto incrementing on the id value at the database end. So by not specifying the id, we are not updaing, and
//the id will be auto generated. After the push has been made, the system will assign the id for me
$user->name = "bob";
$user->pass = "bobpass";
//the system automatically constructs child objects for you if they are not yet constructed, because
//it knows what type should be constructed. So I don't need to construct the address object, manually!
$user->address->line1 = "awesome drive";
$user->address->zip = "90051";
//save to storage, but don't fire webhooks and ignore access checks. Note that the ORMCrudOptions object
//is passed to child objects too when recursion happens, meaning that the same options are inherited by child objects
$user->pushThis(ORMCrudOptions::n()->fireWebhooks(false)->ignoreAccessChecks(true));
echo $user->id; //this will display the auto generated id
echo $user->address->id; //this will be the audo generated id of the address object.
//next lets update something within the object
$user->name = "bob updated";
//because we know now that the object has an id value, it will update the existing object. Remembering tha the User
//object is persistent!
$user->pushThis(ORMCrudOptions::n()->fireWebhooks(false)->ignoreAccessChecks(true));
echo $user->id; //this will be the exact same id as before
echo $user->address->id; //this will be a NEW ID! Remember, the address object is NOT persistent meaning that a new
//instance was created in order to ensure that is is infact non-persistent. The system does handle cleaning up of loose
//objects although this is one of the main perforance problems
//finding the above object by user->name
$user = User::findOne(ORMCrudOptions::n()->query("this.name = ('bob')"));
if($user) {
echo $user->name; //provided that a user with name "bob" exsists, this would output "bob"
}
//finding the above user by address->zip
$user = User::findOne(ORMCrudOptions::n()->query("this.address.zip = ('90051')"));
if($user) {
echo $user->address->zip; //provided that the user with address->zip "90051" exists, this would output "90051"
}
//removing the above user
$user = User::findById(1); //assuming that the id of the user id 1
//add a favourite book to the user
$user->favouriteBook->name = "awesome book!";
//update
$user->pushThis(ORMCrudOptions::n()->ignoreAccessChecks(true));
//remove
$user->removeThis(ORMCrudOptions::n()->ignoreAccessChecks(true));
//with how persistence works, this will delete the user, and the user's address (because the address is non-persistence)
//but will leave the created book un-deleted, because books are persistent and may exist as child objects to other objects
//finally, constructing from document-oriented
$user = User::constructFromArray(array(
"user" => "bob",
"pass" => "passbob",
"address" => array(
"line1" => "awesome drive",
"zip" => "90051"
)
));
//this will only CONSTRUCT the object based on the internal properties defined property types and object types.
//properties that don't exist in the model's defined properties, but exist in the array will be ignored, so having more
//properties in the array than should be there doesn't matter
$user->pushThis(ORMCrudOptions::n()->ignoreAccessChecks(true));
//update only one property of a user object using arrays (this is ultimately how the api system of the ORM was built)
$user = User::constructFromArray(array(
"id" => 1,
"user" => "bob updated"
));
echo $user->pass; //this would output passbob, because the pass was not specified in the array, it was pulled form storage
这里真的不可能展示,但是让这个系统使用起来很愉快的一件事是 class 文件的生成如何使它们非常 IDE 友好(在特别是 auto-completion)。是的,一些 old-school 开发人员会反对这个 new-modern-fangled-technology,但最终当你处理极其复杂的 object-oriented 数据结构时,IDE帮助您正确拼写 属性 名称并获得正确的结构可以是 life-saver!
如果你还在我身边,谢谢你的阅读。不过您可能想知道,您又想要什么?。
简而言之,我在 document/object 存储方面没有太多经验,但在过去几天里,我已经看到有一些技术可以帮助我实现目标是我想做的。我还不能 100% 确定我找到了合适的。我是否创建一个新的 ORM,我能否有效地从现有的 ORM 中获取此功能,我是否使用专用的 object/graph 数据库?
我非常欢迎任何和所有的建议选项!
I am constructing a PHP OOP framework from scratch (I have no choice
in this matter)
你总是有选择的。
The framework is required to handle object-oriented data in the most
efficient way possible. It doesn't need to be lightning quick, it just
needs to be the best possible solution to the problem
我个人会选择序列化字符串或 ORM+MySQL (InnoDB)
Objects very closely resemble class instances, in that they are an
instance of a specific class, which contains a strict set of
properties.
听起来像是……对象的定义。因为对象是 class 的实例,所以它必须类似于 class 结构。此外,class' instance
和 object
是同一回事。所以你有点说 Objects ... resemble objects
.
Object properties can be basic types (strings, numbers, bools) but can
also be one object instance, or an array of objects (with the
restriction that the array must be objects of the same type)
是的,这就是面向对象编程的目的,也是它的强大功能之一。
仍然感觉这是一个嵌套的集合算法,因为你的数据总是适合一个层次结构。简单类型(字符串、整数等)的层次结构深度为 1,而像 customer.address.postcode
这样的对象表达式(来自您的相关 post)将具有每个组件的层次结构级别(在本例中为 3,对应的字符串值存储在最外层节点)。
这个层级似乎可以存储不同的类型,所以你需要对嵌套集合算法做一点小改动。不是每个节点都携带 class-特定(地址、用户等)列,而是有一个对该类型的字符串引用和一个引用它的整数主键。这意味着您不能对数据库的这一部分使用外键约束,但这是一个很小的代价。 (这样做的原因是单个列不能服从几个约束之一,它必须服从所有约束。也就是说,你可以用 pre-insert/pre-update 触发器做一些聪明的事情)。
所以,如果你要使用 Doctrine 或 Propel NestedSet 行为,你会这样定义 tables:
- 节点
- [嵌套设置列,在 ORM 中为您完成]
name
(varchar,记录元素名称如customer
)
is_persistent
(布尔值)
table_name
(varchar)
primary_key
(整数)
- 地址
- (您常用的专栏,同上任何其他 table)
现在,我们这里出现了一个有趣的 属性:在创建层次结构时,您会看到叶节点中的琐碎值可以通过我们的参考系统共享。事实上,我不完全确定 is_persistent
布尔值是必需的:由于共享外部 table 行,它是持久的(如果我没有正确理解你的术语),如果不是,它是非持久的.
所以,如果 customer1.address.postcode
有一个特定的字符串值,您可以让 customer2.address.postcode
指向相同的东西。当更新第一个表达式指向的版本时,第二个表达式将更新 "automatically"(因为它解析到相同的 table 行)。
这里的优势在于,这将在不需要太多工作的情况下与 Propel 和 Doctrine 结合,并且根本不需要任何核心黑客攻击。您需要做一些工作才能将 object/array 转换为层次结构,但这可能不需要太多代码。
附录:让我更多地解释一下我对嵌套元素存储的想法。你说你相信你需要在不同地方的不同层次上共享一个层次结构,但我不太确定(目前我认为你需要一些鼓励不要建立一个过于复杂的系统!)。让我们看一个例子,一个用户有一本最喜欢的书。
为了存储它,我们创建了这些层次结构:
user
node level 1
points to user record containing id=1, name=bob, pass=bobpass
favouriteBook
node level 2
points to book record containing id=1, name=awesome book
author
node level 3
points to author record containing id=3, name=peter, pass=peterpass
现在,假设我们有另一个用户并且想要分享同一作者的另一本最喜欢的书(即我们正在分享 user.favouriteBook.author
)。
user
node level 1
points to different user record containing id=100, name=halfer, pass=halferpass
favouriteBook
node level 2
points to different book record containing id=101, name=textbook
author
node level 3
points to same author record (id = 3)
如果两个用户共享同一本最喜欢的书呢?没问题(我们另外分享user.favouriteBook
):
user
node level 1
points to different user record containing id=101, name=donny, pass=donnypass
favouriteBook
node level 2
points to previous book record (id=1)
author
node level 3
points to previous author record (id = 3)
对这种方法的一个批评是,如果你制作 user.favouriteBook
"persistent"(即共享),那么它应该自动共享 user.favouriteBook.author
。这是因为如果两个或两个以上的人喜欢同一本书,那么所有的人都将是同一作者。
但是,我在评论中指出了为什么我认为我的显式方法更好:替代方案可能是嵌套集的嵌套集,这可能会变得太复杂,而且我认为您还没有证明你需要那个。权衡是我的方法需要更多的存储空间,但我认为这很好。您还有一些更多的对象设置,但是如果您为此拥有一个工厂,并对其进行可靠的单元测试,我认为您不必担心。
(我认为我的方法也可以更快,但如果不为两者开发原型并在真实数据集上测量性能就很难说)。
附录 2,清理一些评论讨论并将其保留为问题上下文中的答案。
要确定我在此处概述的建议是否可行,您需要创建一个原型。我建议使用现有的嵌套集解决方案,例如带有 NestedSetBehaviour 的 Propel,尽管 GitHub 将有许多其他库可供您尝试。在此阶段不要尝试将此原型集成到您自己的 ORM 中,因为集成工作只会分散注意力。目前你想测试这个想法的可行性,仅此而已。
经过一些绝妙的建议,以及由于可能最终解决我的问题而兴奋的不眠之夜,我意识到我仍然没有找到解决方案。所以,我在这里更详细地概述我的问题,希望有人知道实现这一目标的最佳方法。
回顾一下(如果你还没有读过
- 我正在从头构建一个PHP OOP框架(我在这件事上别无选择)
- 框架需要以最有效的方式处理 object-oriented 数据。它不需要快如闪电,只需要是问题的最佳解决方案
- Objects 非常类似于严格编写的 oop objects,因为它们是特定 class 的实例,其中包含一组严格的属性。
- Object 属性可以是基本类型(字符串、数字、布尔值),但也可以是一个 object 实例,或 object 的数组(限制是数组必须是object个相同类型的)
最终,支持 document-oriented 存储的存储引擎(类似于 XML 或 JSON),其中 object 本身具有严格的结构。
我不会概述到目前为止我尝试过的内容(我在之前的 post 中简要讨论过)我将在本文的其余部分 post 详细解释它是什么我正在尝试这样做。 post 会很长(抱歉!)。
首先,我需要讨论一个术语,我必须引入该术语才能解决需求集带来的最关键问题之一。我将此术语命名为 "persistence"。我知道这个术语在处理 object 数据库时确实有不同的含义,因此我愿意接受关于不同术语的建议。但现在,让我们继续吧。
坚持
持久性是指object的独立性。在考虑从 XML 生成的数据结构时,我发现有必要引入这个术语(这是我必须能够做的事情)。在 XML 中,我们看到 object 完全依赖于它们的 parent,同时我们也看到 object 可以独立于 parent object.
以下示例是 XML 文档的示例,它符合特定结构(例如,.wsdl 文件)。每个 object 都类似于具有严格结构的类型。 每个object都有一个"id"属性
在上面的例子中,我们看到了两个用户。两者在 "address" 属性 下都有自己的地址 object。但是,如果我们查看它们的 "favouriteBook" 属性,我们可以看到它们都是 re-use 相同的 Book object 实例。另请注意,这些书使用同一作者。
所以我们的地址 object 是 non-persistent 因为它只与它的 parent object (用户)意味着它的实例只需要在拥有用户 object 存在时存在。然后 Book object 是 persistent 因为它可以在多个位置使用并且它的实例保持持久。
起初,我觉得想出这样一个术语有点疯狂,但是,我发现它非常容易理解和实际使用。它最终将 "many-to-many, one-to-many, one-to-one, many-to-one" 公式浓缩为一个简单的想法,我认为它在嵌套数据中效果更好。
我在这里对上述数据进行了图像表示:
根据我对持久性的定义,有一组规则可以帮助理解它。这些规则如下:
- update/create
- 持久化 child object 正在存储基础 object 更新 持久化 object 的属性,最终更新其实例。
- Non-persistent objects 总是 创建一个新的 object 实例以确保它们始终使用 non-persistent实例(在任何给定时间没有两个 non-persistent 个实例存在于超过一个地方)
- 正在删除
- 持久性 child object 基 object 不会被递归删除。这是因为持久化 object 可能存在于其他地方。您总是会直接删除持久性 object。
- Non-persistent child object 基数 object 与基数 object 一起被删除。如果不将它们移除,它们将被搁置,因为它们的设计要求具有 parent.
- 正在检索
- 由于持久性主要定义了修改的工作方式,因此检索与持久性关系不大,除了您期望持久性如何影响模型的存储方式以及模型的检索方式(持久性object 实例无论位于何处都保持持久性,non-persistent object 总是有自己的实例)
在我们继续之前要注意的最后一件事 - 数据模型的持久性由模型本身定义 而不是关系 。最初,持久性是关系的一部分,但当系统期望您了解模型的结构以及它们的使用方式时,这是完全没有必要的。最终,模型的每个模型实例要么是持久的,要么不是。
所以现在看看一些代码,您可能会开始看到疯狂背后的方法。虽然看起来这个解决方案的原因是能够围绕 objective 符合条件的数据构建一个存储系统,但它的设计实际上来自于希望能够存储 class 个实例, and/or 从 objective 数据结构生成 class 个实例。
我已经写了一些 pseudo-classes 作为我试图产生的功能的例子。我已经评论了大多数方法,包括类型声明。
因此,首先,这将是所有模型 class 扩展的基础 class。 class 的目的是在模型 class/object 和 database/storage 引擎之间创建一个层:
<?php
/**
* This is the base class that all models would extend. It contains the functionalities that are useful among all model
* objects, such as crud actions, finding, and crud event management.
*
* @author Donny Sutherland <donny@pixelpug.co.uk>
* @package Main
* @subpackage Sub
*
* Class ORMModel
*/
class ORMModel {
/**
* In order to generate relationships between objects, every object MUST have an id. This functions as the object's
* unique identifier. Each object in it's model type (collection) has it's own id.
*
* @var int
*/
public $id;
/**
* Internal property assigned by the application. This is where the persistence of the model is defined.
*
* @var bool
*/
protected $internal_isPersistent = true;
/**
* Internal property assigned by the application. This is an array of the model's properties, and their PHP type.
*
* For example, a User model might use something like this:
* array(
"id" => "integer",
* "username" => "string",
* "password" => "string",
* "address" => "object",
* "favouriteBook" => "object",
* "allBooks" => "array"
* )
*
* @var array
*/
protected $internal_propertyTypes = array();
/**
* Internal property assigned by the application. This is an array of the model's properties which are objects, and
* the MODEL CLASS type of the object.
*
* For example, the User model example for the property types might use this:
* array(
* "address" => "Address",
"favouriteBook" => "Book",
* "allBooks" => "Book"
* )
*
* @var array
*/
protected $internal_objectTypes = array();
/**
* I am not 100% sure on the best way to use this yet, I have tried a few different ways and all seem to cause
* performance problems. But ultimately, before we attempt to update an object, we cache it's currently stored
* instance to this property, allowing us to compare old vs new. I find this really useful for detecting whether a
* property has changed, I just need to work out the best way to do it.
*
* @var $this
*/
protected $internal_old;
/**
* The lazy way to construct an empty model object (all NULL values)
*
* @return $this
*/
final public static function constructEmpty() {
}
/**
* This method is used by the other constructFromXXX methods once the data has been converted to a PHP array.
* This method is what allows us to build a RESTful interface into the ORM system as it conforms to the following
* rules:
*
* - if the id is set (not null), first pull the object from storage.
* - For each key => value of the passed array, OVERWRITE the value
* - For properties that are model objects/arrays, if the property is assiged to the array:
* - if the array value is NULL, we are clearing the object relationship
* - if the array valus is not null, construct recursively at this point
*
* Ultimately, if you assign a property in the array that you pass to this method, it will overwrite the value. If
* you do not, it will use the property value in storage.
*
* @param array $array
*
* @return $this
*/
final public static function constructFromArray(array $array) {
}
/**
* This method attempts to decode the value of $json into a PHP array. It then calls constructFromArray if the string
* could be decoded.
*
* @param $json
*
* @return $this
*/
final public static function constructFromJson($json) {
}
/**
* This method attempts to decode the value of $xml into a PHP array. It then calls constructFromArray if the xml
* could be decoded.
*
* @param $xml
*
* @return $this
*/
final public static function constructFromXml($xml) {
}
/**
* Find one object, based on a set of options.
*
* @param ORMCrudOptions $options
*
* @return $this
*/
final public static function findOne(ORMCrudOptions $options) {
}
/**
* Find all objects, (optionally) based on a set of options
*
* @param ORMCrudOptions $options
*
* @return $this[]
*/
final public static function findAll(ORMCrudOptions $options=null) {
}
/**
* Find the count of objects, based on a set of optoins
*
* @param ORMCrudOptions $options
*
* @return integer
*/
final public static function findCount(ORMCrudOptions $options) {
}
/**
* Find one object, based on it's id, and (optionally) a set of options.
*
* @param ORMCrudOptions $options
*
* @return $this
*/
final public static function findById($id,ORMCrudOptions $options=null) {
}
/**
* Push this object to storage. This creates/updates all of the contained objects, based on their id's and
* persistence.
*
* @param ORMCrudOptions $options
*
* @return bool
*/
final public function pushThis(ORMCrudOptions $options) {
}
/**
* Pull this object form storage. This retrieves all of the contained objects again, based on their id's and
* persistence.
*
* @param ORMCrudOptions $options
*
* @return bool
*/
final public function pullThis(ORMCrudOptions $options) {
}
/**
* Remove this object from storage. This conditionally removes the contained objects (based on persistence) based
* on their id's.
*
* @param ORMCrudOptions $options
*/
final public function removeThis(ORMCrudOptions $options) {
}
/**
* This is a crud event.
*/
public function beforeCreate() {
}
/**
* This is a crud event.
*/
public function afterCreate() {
}
/**
* This is a crud event.
*/
public function beforeUpdate() {
}
/**
* This is a crud event.
*/
public function afterUpdate() {
}
/**
* This is a crud event.
*/
public function beforeRemove() {
}
/**
* This is a crud event.
*/
public function afterRemove() {
}
/**
* This is a crud event.
*/
public function beforeRetrieve() {
}
/**
* This is a crud event.
*/
public function afterRetrieve() {
}
}
因此,最终,此 class 旨在提供构建、查找、保存、检索和删除模型 object 的功能。内部属性是仅存在于 classes 中(不在存储中)的属性。当您使用界面创建模型并将 property/fields 添加到模型时,这些属性由框架本身填充。
想法是,该框架带有一个用于管理数据模型的界面。使用此界面,您可以创建模型,并将 property/fields 添加到模型中。这样做时,系统会自动为您创建 class 文件,并在您修改持久性和 属性 类型时更新这些内部属性。
为了让开发人员更友好,系统为每个模型创建了两个 class 文件。一个基础 class(扩展了 ORMModel)和另一个 class(扩展了基础 class)。基数 class 由系统操纵,因此不建议修改此文件。另一个 class 被开发人员用来为模型和 crud 事件添加额外的功能。
回到示例数据,这里是用户群 class:
<?php
class User_Base extends ORMModel {
public $name;
public $pass;
/**
* @var Address
*/
public $address;
/**
* @var Book
*/
public $favouriteBook;
protected $internal_isPersistent = true;
protected $internal_propertyTypes = array(
"id" => "integer",
"name" => "string",
"pass" => "string",
"address" => "object",
"favouriteBook" => "object"
);
protected $internal_objectTypes = array(
"address" => "Address",
"favouriteBook" => "Book"
);
}
几乎不言自明。再次注意,内部属性是由系统生成的,因此这些数组将根据您在模型管理界面中 creating/modifying User 模型时指定的 property/fields 生成。还要注意地址上的文档块和 favouriteBook
属性 定义。这些也是由系统生成的,使 classes 非常 IDE 友好。
这将是为用户模型生成的另一个 class:
<?php
final class User extends User_Base {
public function beforeCreate() {
}
public function afterCreate() {
}
public function beforeUpdate() {
}
public function afterUpdate() {
}
public function beforeRemove() {
}
public function afterRemove() {
}
public function beforeRetrieve() {
}
public function afterRetrieve() {
}
}
再一次,非常不言自明。我们扩展了基础 class 以创建另一个 class,开发人员可以在其中添加其他方法,并向 crud 事件添加功能。
我不会添加构成其余示例数据的其他 object。由于上面应该解释了它们的外观。
所以你 may/may 没有注意到在 ORMModel class 中,CRUD 方法需要一个 ORMCrudOptions class 的实例。这个 class 对整个系统来说非常重要,所以让我们快速看一下:
<?php
/**
* Despite this object being some-what aggregate, it it quite possibly the most important part of the ORM, in that it
* defines how CRUD actions are executed, and outline how the querying is done.
*
* Class ORMCrudOptions
*/
final class ORMCrudOptions {
/**
* This ultimately makes up the "where" part of the sql query. However, because we want to be able to make querying
* possible at any depth within the hierarchy of a model, this gets quite complicated.
*
* Previously, I developed a system which allowed the user to do something like this:
*
* "this.customer.address.postcode LIKE ('%XXX%') OR this.customer.address.line1 LIKE ('%XXX%')
*
* he "this" and the "." are my extension to basic sql. The "this" refers to the base model that you are finding,
* and each "." basically drills down into the hierarchy to make a comparison on a property somewhere within a
* contained model object.
*
* I will explain more how I did this in my post, I am most definitely looking at how I could better achieve this
* though.
*
* @var string
*/
private $query;
/**
* This allows you to build up a list of order by definitions.
*
* Using the orderBy method, you can chain up the order by statements like:
*
* ->orderBy("this.name","asc")->orderBy("this.customer.address.line1","desc")
*
* Which would be similar to doing:
*
* ORDER BY this_name ASC, this_customer_address.line1 DESC
*
* @var array
*/
private $orderBy;
/**
* This allows you to set the limit start and limit values by doing:
*
* ->limit(10,10)
*
* Which would be similar to doing:
*
* LIMIT 10, 10
*
* @var
*/
private $limit;
/**
* Depth was added in my later en devours to try and help with performance. It allows you to specify the depth at
* which to retrieve data. Although this helped with optimisation a lot, I really disliked having to use
* implement this because it seems like a work-around. I would rather be able to increase performance elsewhere so
* that objects are always retrieved at their full depth
*
* @var integer
*/
private $depth;
/**
* This was another newly added feature. Whenever you execute a crud action on a model, the model instance is stored
* in a local cache if this is true, and/or retrieved from this cached if this value is true.
*
* I did find this to make a significant increase on performance, although it did bring in complications that make
* the system tricky to use at times. You really need to understand how and when to use the cache, otherwise it can
* be infuriatingly obtuse.
*
* @var bool
*/
private $useCache;
/**
* Built into the ORM system, and tied in with the application I set up a webhook system which fires out webhooks on
* crud events. I discovered the need to be able to disable webhooks at times (when doing large amounts of crud
* actions in one go) pretty early on. Setting this to false basically disables webhooks on the crud action
*
* @var bool
*/
private $fireWebhooks;
/**
* Also build into the application, and tied into the ORM system is an access system. This works on a seperate
* layer to the database, allowing me to use the same access system as I use for everything in the framework as I do
* for defining crud action access. However, in some instances I found it useful to disable access checks.
*
* This is always on by default. In the api system that I built to access the data models, you were not able to
* modify this property and therefore were always subject to access checks.
*
* @var
*/
private $ignoreAccessChecks;
/**
* The lazy way to create a new instance of options.
*
* @return ORMCrudOptions
*/
public static function n() {
return new ORMCrudOptions();
}
/**
* Set the query value
*
* @param $query
*
* @return $this
*/
public function query($query) {
$this->query = $query;
return $this;
}
/**
* Add an orderby field and direction
*
* @param $field
* @param string $direction
*
* @return $this
* @internal param array $orderBy
*
*/
public function orderBy($field,$direction="asc") {
$this->orderBy[] = array($field,$direction);
return $this;
}
/**
* Set the limit start and limit.
*
* @param $limitResults
* @param null $limitStart
*
* @return $this
*/
public function limit($limitResults,$limitStart=null) {
$this->limit = array($limitResults,$limitStart);
return $this;
}
/**
* Set the depth for retrieval
*
* @param $depth
*
* @return $this
*/
public function depth($depth) {
$this->depth = $depth;
return $this;
}
/**
* Set whether to use the model cache
*
* @param $useCache
*
* @return $this
*/
public function useCache($useCache) {
$this->useCache = $useCache;
return $this;
}
/**
* Set whether to fire webhooks on crud actions
*
* @param $fireWebhooks
*
* @return $this
*/
public function fireWebhooks($fireWebhooks) {
$this->fireWebhooks = $fireWebhooks;
return $this;
}
/**
* Set whether to ignore access checks
*
* @param $ignoreAccessChecks
*
* @return $this
*/
public function ignoreAccessChecks($ignoreAccessChecks) {
$this->ignoreAccessChecks = $ignoreAccessChecks;
return $this;
}
}
这个 class 背后的想法是消除在 crud 方法中使用大量参数的需要,因为这些参数中的大多数可以 re-used 在所有粗糙的方法。记下对查询 属性 的评论,因为那很重要。
所以,这几乎涵盖了基础 psuedo-code 和我正在尝试做的事情背后的想法。所以最后,我会展示一些 user-scenarios:
<?php
//the most simple way to store a user
$user = User::constructEmpty();
//we use auto incrementing on the id value at the database end. So by not specifying the id, we are not updaing, and
//the id will be auto generated. After the push has been made, the system will assign the id for me
$user->name = "bob";
$user->pass = "bobpass";
//the system automatically constructs child objects for you if they are not yet constructed, because
//it knows what type should be constructed. So I don't need to construct the address object, manually!
$user->address->line1 = "awesome drive";
$user->address->zip = "90051";
//save to storage, but don't fire webhooks and ignore access checks. Note that the ORMCrudOptions object
//is passed to child objects too when recursion happens, meaning that the same options are inherited by child objects
$user->pushThis(ORMCrudOptions::n()->fireWebhooks(false)->ignoreAccessChecks(true));
echo $user->id; //this will display the auto generated id
echo $user->address->id; //this will be the audo generated id of the address object.
//next lets update something within the object
$user->name = "bob updated";
//because we know now that the object has an id value, it will update the existing object. Remembering tha the User
//object is persistent!
$user->pushThis(ORMCrudOptions::n()->fireWebhooks(false)->ignoreAccessChecks(true));
echo $user->id; //this will be the exact same id as before
echo $user->address->id; //this will be a NEW ID! Remember, the address object is NOT persistent meaning that a new
//instance was created in order to ensure that is is infact non-persistent. The system does handle cleaning up of loose
//objects although this is one of the main perforance problems
//finding the above object by user->name
$user = User::findOne(ORMCrudOptions::n()->query("this.name = ('bob')"));
if($user) {
echo $user->name; //provided that a user with name "bob" exsists, this would output "bob"
}
//finding the above user by address->zip
$user = User::findOne(ORMCrudOptions::n()->query("this.address.zip = ('90051')"));
if($user) {
echo $user->address->zip; //provided that the user with address->zip "90051" exists, this would output "90051"
}
//removing the above user
$user = User::findById(1); //assuming that the id of the user id 1
//add a favourite book to the user
$user->favouriteBook->name = "awesome book!";
//update
$user->pushThis(ORMCrudOptions::n()->ignoreAccessChecks(true));
//remove
$user->removeThis(ORMCrudOptions::n()->ignoreAccessChecks(true));
//with how persistence works, this will delete the user, and the user's address (because the address is non-persistence)
//but will leave the created book un-deleted, because books are persistent and may exist as child objects to other objects
//finally, constructing from document-oriented
$user = User::constructFromArray(array(
"user" => "bob",
"pass" => "passbob",
"address" => array(
"line1" => "awesome drive",
"zip" => "90051"
)
));
//this will only CONSTRUCT the object based on the internal properties defined property types and object types.
//properties that don't exist in the model's defined properties, but exist in the array will be ignored, so having more
//properties in the array than should be there doesn't matter
$user->pushThis(ORMCrudOptions::n()->ignoreAccessChecks(true));
//update only one property of a user object using arrays (this is ultimately how the api system of the ORM was built)
$user = User::constructFromArray(array(
"id" => 1,
"user" => "bob updated"
));
echo $user->pass; //this would output passbob, because the pass was not specified in the array, it was pulled form storage
这里真的不可能展示,但是让这个系统使用起来很愉快的一件事是 class 文件的生成如何使它们非常 IDE 友好(在特别是 auto-completion)。是的,一些 old-school 开发人员会反对这个 new-modern-fangled-technology,但最终当你处理极其复杂的 object-oriented 数据结构时,IDE帮助您正确拼写 属性 名称并获得正确的结构可以是 life-saver!
如果你还在我身边,谢谢你的阅读。不过您可能想知道,您又想要什么?。
简而言之,我在 document/object 存储方面没有太多经验,但在过去几天里,我已经看到有一些技术可以帮助我实现目标是我想做的。我还不能 100% 确定我找到了合适的。我是否创建一个新的 ORM,我能否有效地从现有的 ORM 中获取此功能,我是否使用专用的 object/graph 数据库?
我非常欢迎任何和所有的建议选项!
I am constructing a PHP OOP framework from scratch (I have no choice in this matter)
你总是有选择的。
The framework is required to handle object-oriented data in the most efficient way possible. It doesn't need to be lightning quick, it just needs to be the best possible solution to the problem
我个人会选择序列化字符串或 ORM+MySQL (InnoDB)
Objects very closely resemble class instances, in that they are an instance of a specific class, which contains a strict set of properties.
听起来像是……对象的定义。因为对象是 class 的实例,所以它必须类似于 class 结构。此外,class' instance
和 object
是同一回事。所以你有点说 Objects ... resemble objects
.
Object properties can be basic types (strings, numbers, bools) but can also be one object instance, or an array of objects (with the restriction that the array must be objects of the same type)
是的,这就是面向对象编程的目的,也是它的强大功能之一。
仍然感觉这是一个嵌套的集合算法,因为你的数据总是适合一个层次结构。简单类型(字符串、整数等)的层次结构深度为 1,而像 customer.address.postcode
这样的对象表达式(来自您的相关 post)将具有每个组件的层次结构级别(在本例中为 3,对应的字符串值存储在最外层节点)。
这个层级似乎可以存储不同的类型,所以你需要对嵌套集合算法做一点小改动。不是每个节点都携带 class-特定(地址、用户等)列,而是有一个对该类型的字符串引用和一个引用它的整数主键。这意味着您不能对数据库的这一部分使用外键约束,但这是一个很小的代价。 (这样做的原因是单个列不能服从几个约束之一,它必须服从所有约束。也就是说,你可以用 pre-insert/pre-update 触发器做一些聪明的事情)。
所以,如果你要使用 Doctrine 或 Propel NestedSet 行为,你会这样定义 tables:
- 节点
- [嵌套设置列,在 ORM 中为您完成]
name
(varchar,记录元素名称如customer
)is_persistent
(布尔值)table_name
(varchar)primary_key
(整数)
- 地址
- (您常用的专栏,同上任何其他 table)
现在,我们这里出现了一个有趣的 属性:在创建层次结构时,您会看到叶节点中的琐碎值可以通过我们的参考系统共享。事实上,我不完全确定 is_persistent
布尔值是必需的:由于共享外部 table 行,它是持久的(如果我没有正确理解你的术语),如果不是,它是非持久的.
所以,如果 customer1.address.postcode
有一个特定的字符串值,您可以让 customer2.address.postcode
指向相同的东西。当更新第一个表达式指向的版本时,第二个表达式将更新 "automatically"(因为它解析到相同的 table 行)。
这里的优势在于,这将在不需要太多工作的情况下与 Propel 和 Doctrine 结合,并且根本不需要任何核心黑客攻击。您需要做一些工作才能将 object/array 转换为层次结构,但这可能不需要太多代码。
附录:让我更多地解释一下我对嵌套元素存储的想法。你说你相信你需要在不同地方的不同层次上共享一个层次结构,但我不太确定(目前我认为你需要一些鼓励不要建立一个过于复杂的系统!)。让我们看一个例子,一个用户有一本最喜欢的书。
为了存储它,我们创建了这些层次结构:
user
node level 1
points to user record containing id=1, name=bob, pass=bobpass
favouriteBook
node level 2
points to book record containing id=1, name=awesome book
author
node level 3
points to author record containing id=3, name=peter, pass=peterpass
现在,假设我们有另一个用户并且想要分享同一作者的另一本最喜欢的书(即我们正在分享 user.favouriteBook.author
)。
user
node level 1
points to different user record containing id=100, name=halfer, pass=halferpass
favouriteBook
node level 2
points to different book record containing id=101, name=textbook
author
node level 3
points to same author record (id = 3)
如果两个用户共享同一本最喜欢的书呢?没问题(我们另外分享user.favouriteBook
):
user
node level 1
points to different user record containing id=101, name=donny, pass=donnypass
favouriteBook
node level 2
points to previous book record (id=1)
author
node level 3
points to previous author record (id = 3)
对这种方法的一个批评是,如果你制作 user.favouriteBook
"persistent"(即共享),那么它应该自动共享 user.favouriteBook.author
。这是因为如果两个或两个以上的人喜欢同一本书,那么所有的人都将是同一作者。
但是,我在评论中指出了为什么我认为我的显式方法更好:替代方案可能是嵌套集的嵌套集,这可能会变得太复杂,而且我认为您还没有证明你需要那个。权衡是我的方法需要更多的存储空间,但我认为这很好。您还有一些更多的对象设置,但是如果您为此拥有一个工厂,并对其进行可靠的单元测试,我认为您不必担心。
(我认为我的方法也可以更快,但如果不为两者开发原型并在真实数据集上测量性能就很难说)。
附录 2,清理一些评论讨论并将其保留为问题上下文中的答案。
要确定我在此处概述的建议是否可行,您需要创建一个原型。我建议使用现有的嵌套集解决方案,例如带有 NestedSetBehaviour 的 Propel,尽管 GitHub 将有许多其他库可供您尝试。在此阶段不要尝试将此原型集成到您自己的 ORM 中,因为集成工作只会分散注意力。目前你想测试这个想法的可行性,仅此而已。