【发布时间】:2015-04-14 14:59:02
【问题描述】:
经过一些绝妙的建议,以及由于最终有可能解决我的问题而兴奋的不眠之夜,我意识到我仍然没有完全找到解决方案。所以,我在这里更详细地概述我的问题,希望有人知道实现这一目标的最佳方法。
回顾一下(如果你还没有读过the previous post):
- 我正在从头构建一个PHP OOP框架(这件事我没有选择)
- 需要该框架以尽可能最有效的方式处理面向对象的数据。它不需要闪电般快速,它只需要成为问题的最佳解决方案
- 对象与严格编写的 oop 对象非常相似,因为它们是特定类的实例,其中包含一组严格的属性。
- 对象属性可以是基本类型(字符串、数字、布尔值),但也可以是一个对象实例或对象数组(限制数组必须是相同类型的对象)
最终,一个支持面向文档的存储(类似于 XML 或 JSON)的存储引擎,其中对象本身具有严格的结构。
我将在这篇文章的其余部分详细解释我正在尝试做的事情,而不是概述我迄今为止所做的尝试(我在上一篇文章中对此进行了简要讨论)。这篇文章会很长(抱歉!)。
首先,我需要讨论一个必须引入的术语,以解决一组需求带来的最关键问题之一。我将这个术语命名为“持久性”。我知道这个术语在处理对象数据库时确实有不同的含义,因此我愿意接受关于不同术语的建议。但现在,让我们继续前进吧。
持久性
持久性是指对象的独立性。在考虑从 XML 生成的数据结构时,我发现有必要引入这个术语(这是我必须能够做到的)。在 XML 中,我们看到完全依赖于其父对象的对象,同时我们也看到可以独立于父对象的对象。
以下示例是符合特定结构的 XML 文档示例(例如,.wsdl 文件)。每个对象都类似于具有严格结构的类型。 每个对象都有一个“id”属性
在上面的例子中,我们看到了两个用户。两者在其“地址”属性下都有自己的地址对象。但是,如果我们查看它们的“favouriteBook”属性,我们可以看到它们都重用同一个 Book 对象实例。另请注意,这些书籍使用同一作者。
所以我们有一个非持久性的 Address 对象,因为它只与它的父对象(用户)相关,这意味着它的实例只需要在拥有的用户对象存在时存在。然后是 持久性 的 Book 对象,因为它可以在多个位置使用,并且它的实例保持持久性。
起初,我对想出这样一个术语感到有点疯狂,但是,我发现它非常易于理解和实际使用。它最终将“多对多、一对多、一对一、多对一”的公式浓缩为一个简单的想法,我觉得它更适合嵌套数据。
我在这里对上述数据进行了图像表示:
根据我对持久性的定义,有一套规则可以帮助理解它。这些规则如下:
- 更新/创建
- 正在存储的基础对象的持久子对象更新持久对象的属性,最终更新其实例。
- 非持久对象总是创建对象的新实例以确保它们始终使用非持久实例(在任何给定时间,没有两个非持久实例存在于多个位置)
- 正在删除
- 基础对象的持久子对象不会被递归删除。这是因为持久对象可能存在于其他地方。您总是会直接删除持久对象。
- 基础对象的非持久性子对象与基础对象一起被移除。如果它们没有被移除,它们就会被搁置,因为它们的设计要求它们有一个父节点。
- 正在检索
- 由于持久性主要定义了修改的工作方式,因此检索并不涉及很多持久性,除了您期望持久性如何影响模型的存储方式以及如何检索模型(持久性对象实例在任何地方都保持持久性)位于,非持久对象总是有自己的实例)
在我们继续之前要注意的最后一件事 - 数据模型的持久性是由模型本身定义的而不是关系。最初,持久性是关系的一部分,但是当系统希望您知道模型的结构以及如何使用它们时,这完全没有必要。最终,模型的每个模型实例要么是持久的,要么不是。
所以现在看一些代码,你可能会开始看到疯狂背后的方法。虽然看起来这个解决方案的原因是能够围绕符合一组条件的客观数据构建一个存储系统,但它的设计实际上来自希望能够存储类实例,和/或从一个客观的数据结构。
我已经编写了一些伪类作为我正在尝试生成的功能的示例。我已经注释了大多数方法,包括类型声明。
首先,这将是所有模型类都将扩展的基类。这个类的目的是在模型类/对象和数据库/存储引擎之间创建一个层:
<?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() {
}
}
因此,最终,此类将被设计为提供构造、查找、保存、检索和删除模型对象的功能。内部属性是仅存在于类中(不在存储中)的属性。当您使用接口创建模型并向模型添加属性/字段时,框架本身会填充这些属性。
这个想法是,该框架带有一个用于管理数据模型的接口。使用此界面,您可以创建模型,并将属性/字段添加到模型中。这样做时,系统会自动为您创建类文件,并在您修改持久性和属性类型时更新这些内部属性。
为了让开发人员友好,系统会为每个模型创建两个类文件。一个基类(扩展 ORMModel)和另一个类(扩展基类)。基类由系统操作,因此不建议修改此文件。开发人员使用另一个类来为模型和 crud 事件添加额外的功能。
回到示例数据,这里是 User 基类:
<?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"
);
}
几乎不言自明。再次注意,内部属性是由系统生成的,因此这些数组将根据您在模型管理界面中创建/修改用户模型时指定的属性/字段生成。还要注意地址和favouriteBook 属性定义上的文档块。这些也是由系统生成的,使这些类对 IDE 非常友好。
这将是为 User 模型生成的另一个类:
<?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() {
}
}
再次,非常不言自明。我们扩展了基类以创建另一个类,开发人员可以在其中添加其他方法,并向 crud 事件添加功能。
我不会添加构成示例数据其余部分的其他对象。因为上面应该解释了它们的外观。
所以您可能/可能没有注意到,在 ORMModel 类中,CRUD 方法需要 ORMCrudOptions 类的实例。这个类对整个系统非常重要,所以让我们快速看一下:
<?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;
}
}
这个类背后的想法是消除在 crud 方法中包含大量参数的需要,因为这些参数中的大多数可以在所有 crud 方法中重用。记下查询属性上的 cmets,因为这很重要。
所以,这几乎涵盖了我正在尝试做的事情背后的基本伪代码和想法。所以最后,我将展示一些用户场景:
<?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
这里不太可能展示,但让这个系统使用起来很愉快的一件事是类文件的生成如何使它们对 IDE 非常友好(特别是对于自动完成)。是的,一些老派开发人员会反对这种新奇的现代技术,但归根结底,当您处理疯狂复杂的面向对象数据结构时,让 IDE 帮助您拼写您的财产正确的名称和正确的结构可以挽救生命!
如果你还在我身边,谢谢你的阅读。不过,您可能想知道,您又想要什么?。
简而言之,我在文档/对象存储方面没有大量经验,而且在过去的几天里,我已经被证明有一些技术可以帮助我实现自己的目标试图做。我只是不能 100% 确定我找到了正确的。我是否要创建一个新的 ORM,是否可以有效地从现有的 ORM 中获取此功能,是否使用专用的对象/图形数据库?
我非常欢迎任何和所有的建议!
【问题讨论】:
-
这是我旧帖子的扩展,这里:stackoverflow.com/questions/28482648/…
-
请更简洁。或者至少在问题的顶部放置一个要点摘要。
-
我最初的反应是“持久性”在这里是错误的术语,因为它已经被使用了——它本质上意味着存储一些东西。需要记住它意味着不同的东西可能会有点令人困惑,尤其是在存储系统的上下文中!不过不用担心,我想我可以解决这个问题。我对上一个问题的嵌套设置想法仍然有效,稍后我会添加一些额外的细节。
-
@SverriM.Olsen 我不禁觉得我已经这样做了?您能否详细说明您建议我如何改进这篇文章?非常感谢
-
@halfer - 可以理解。我真的在为 3k 的限制而苦苦挣扎!我必须实际提供链接而不是在内容中包含文件,但是我目前也仅限于每个帖子 2 个链接。我会仔细阅读帖子,看看我是否可以将 xml 文件内容放入其中。再次感谢