经过一些奇妙的建议,以及由于最终有可能解决我的问题而兴奋的不眠之夜,我意识到我仍然没有完全找到解决方案。因此,我在这里更详细地概述我的问题,希望有人知道实现这一目标的最佳方法。
回顾一下(如果你还没有读过上一篇文章 https://stackoverflow.com/questions/28482648/how-to-store-object-based-data-in-a-database-so-it-remains-queryable):
- 我正在从头开始构建一个 PHP OOP 框架(在这件事上我别无选择)
- 该框架需要以尽可能最有效的方式处理面向对象的数据。它不需要快如闪电,它只需要成为问题的最佳解决方案即可
- 对象非常类似于严格编写的 oop 对象,因为它们是特定类的实例,该类包含一组严格的属性。
- 对象属性可以是基本类型(字符串、数字、布尔值),但也可以是一个对象实例或一组对象(限制是该数组必须是同一类型的对象)
最终,存储引擎支持面向文档的存储(类似于 XML 或 JSON),其中对象本身具有严格的结构。
我不会概述到目前为止我已经尝试过的事情(我在上一篇文章中简要讨论了这一点),我将在这篇文章的其余部分详细解释我正在尝试做的事情。这篇文章会很长(抱歉!)。
首先,我需要讨论一个术语,我必须引入该术语来解决这组需求带来的最关键的问题之一。我把这个术语命名为“坚持”。我知道这个术语在处理对象数据库时确实有不同的含义,因此我愿意接受关于不同术语的建议。但现在,让我们继续前进。
坚持
持久性是指对象的独立性。我发现在考虑从 XML 生成的数据结构时需要引入这个术语(这是我必须能够做到的)。在 XML 中,我们看到完全依赖于其父对象的对象,同时我们也看到可以独立于父对象的对象。
下面的示例是符合特定结构的 XML 文档示例(例如 .wsdl 文件)。每个对象都类似于具有严格结构的类型。每个对象都有一个“id”属性
在上面的示例中,我们看到两个用户。两者在其“地址”属性下都有自己的地址对象。但是,如果我们查看他们的“favouriteBook”属性,我们可以看到他们都re-use相同的 Book 对象实例。另请注意,这些书使用同一作者。
所以我们有 Address 对象非持久因为它只与其父对象(用户)相关,这意味着它的实例只需要在拥有的用户对象存在时存在。然后是 Book 对象执着的因为它可以在多个位置使用并且它的实例保持持久性。
起初,我觉得提出这样的术语有点疯狂,但是,我发现它非常容易理解和实际使用。它最终将“多对多、一对多、一对一、多对一”公式浓缩为一个简单的想法,我认为这种想法对于嵌套数据来说效果更好。
我在这里对上述数据进行了图像表示:
根据我对持久性的定义,有一组规则可以帮助理解它。这些规则如下:
- 更新/创建
- 正在存储的基础对象的持久子对象update持久对象的属性,最终更新其实例。
- 非持久对象always创建对象的一个新实例,以确保它们始终使用非持久实例(在任何给定时间,两个非持久实例都不会存在于多个位置)
- deleting
- 基础对象的持久子对象不会被递归删除。这是因为持久对象可能存在于其他地方。您总是会直接删除持久对象。
- 基础对象的非持久子对象将与基础对象一起删除。如果不将它们移除,它们就会陷入困境,因为它们的设计要求它们有一个父级。
- 检索
- 由于持久性主要定义了修改的工作方式,因此除了您期望持久性如何影响模型的存储方式以及如何检索模型之外,检索并不涉及太多的持久性(持久性对象实例无论位于何处都保持持久性,非持久对象总是有自己的实例)
在我们继续之前要注意的最后一件事 - 数据模型的持久性是由模型本身定义的而不是关系。最初,持久性是关系的一部分,但当系统期望您了解模型的结构以及它们的使用方式时,这是完全没有必要的。最终,模型的每个模型实例要么是持久的,要么不是。
因此,现在看一些代码,您可能会开始看到疯狂背后的方法。虽然这个解决方案的原因似乎是能够围绕符合一组条件的客观数据构建一个存储系统,但它的设计实际上来自于希望能够存储类实例,和/或从客观的数据结构。
我编写了一些伪类作为我尝试生成的功能的示例。我已经注释了大多数方法,包括类型声明。
首先,这将是所有模型类将扩展的基类。此类的目的是在模型类/对象和数据库/存储引擎之间创建一个层:
<?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 <[email protected] /cdn-cgi/l/email-protection>
* @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 友好。
这将是为用户模型生成的另一个类:
<?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 方法中重复使用。请记下查询属性上的注释,因为这一点很重要。
因此,这几乎涵盖了我正在尝试做的事情背后的基本伪代码和想法。最后,我将展示一些用户场景:
<?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 中有效地获取此功能,我是否使用专用的对象/图形数据库?
我非常欢迎任何和所有的建议!