经过一些奇妙的建议,以及由于有可能最终解决我的问题而度过一个不眠之夜,我意识到我仍然没有找到解决方案。所以,我在这里更详细地概述我的问题,希望有人知道实现这一目标的最佳方法。
回顾一下(如果您还没有阅读the previous post):
最终,一个支持面向文档的存储(类似于XML或JSON)的存储引擎,其中对象本身具有严格的结构。
而不是概述我到目前为止所尝试的内容(我在上一篇文章中对此进行了简要讨论),我将花费本文的其余部分详细解释我正在尝试做什么。这篇文章会很长(对不起!)。
首先,我需要讨论一个我必须介绍的术语,以解决这些要求中出现的最关键问题之一。我已将此术语命名为" persistence" 。我理解这个术语在处理对象数据库时确实有不同的含义,因此我对不同术语的建议持开放态度。但就目前而言,让我们继续前进。
持久性
持久性是指对象的独立性。在考虑从XML生成的数据结构时,我发现需要引入这个术语(这是我必须要做的事情)。在XML中,我们看到完全依赖于父对象的对象,而我们也看到可以独立于父对象的对象。
以下示例是XML文档的示例,它符合某种结构(例如,.wsdl文件)。每个对象类似于具有严格结构的类型。 每个对象都有一个" id"属性
在上面的例子中,我们看到两个用户。两者都在他们的"地址"下面有自己的地址对象。属性。但是,如果我们看看他们的" favouriteBook"属性,我们可以看到它们都重用相同的Book对象实例。另请注意,这些书使用的是同一作者。
所以我们有一个非持久性的Address对象,因为它只与其父对象(User)相关,这意味着它的实例只需要在拥有的User对象存在时才存在。然后是持久的Book对象,因为它可以在多个位置使用,并且它的实例保持持久。
起初,我觉得有点疯狂,想出这样的术语,然而,我发现它非常简单易懂和实用。它最终浓缩了多对多,一对多,一对一,多对一的#34;这个公式变成了一个简单的想法,我认为嵌套数据效果更好。
我在这里制作了上述数据的图像表示:
根据我对持久性的定义,提出了一套有助于理解它的规则。这些规则如下:
在我们继续之前需要注意的一件事 - 数据模型的持久性由模型本身而不是关系来定义。最初,持久性是关系的一部分,但是当系统期望您知道模型的结构以及它们的使用方式时,这是完全没有必要的。最终,模型的每个模型实例都是持久的,或者不是。
现在看一些代码,你可能会开始看到疯狂背后的方法。虽然看起来这个解决方案的原因似乎是能够围绕符合条件的客观数据构建存储系统,但它的设计实际上来自于希望能够存储类实例和/或生成来自客观数据结构的类实例。
我已经写了一些伪类作为我试图产生的功能的一个例子。我评论了大多数方法,包括类型声明。
首先,这将是所有模型类将扩展的基类。此类的目的是在模型类/对象和数据库/存储引擎之间创建一个层:
<?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事件添加其他功能。
回到示例数据,这里是用户基类:
<?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"
);
}
非常自我解释。再次注意,内部属性由系统生成,因此将根据您在模型管理界面中创建/修改用户模型时指定的属性/字段生成这些数组。另请注意地址上的docblock和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方法中重用。记下查询属性的注释,因为这很重要。
所以,这几乎涵盖了基础伪代码和我想要做的事情背后的想法。最后,我将展示一些用户场景:
<?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中有效地获取此功能,是否使用专用的对象/图形数据库?
我非常欢迎任何建议!
答案 0 :(得分:1)
它仍然感觉这是一个嵌套的集合算法,因为您的数据将始终适合层次结构。简单类型(字符串,整数等)具有深度为1的层次结构,对象表达式如customer.address.postcode
(来自相关帖子)将具有每个组件的层次结构级别(在这种情况下为3,具有相应的字符串)存储在最外层节点中的值。)
这个层次结构似乎可以存储不同的类型,因此您需要对嵌套集算法进行小的更改。您可以使用类型的字符串引用和引用它的整数主键,而不是每个节点都包含特定于类的(地址,用户等)列。这意味着您不能对数据库的这一部分使用外键约束,但这只是一个很小的代价。 (原因是单个列不能服从几个约束中的一个,它必须遵守它们。也就是说,你可以通过预插入/预更新触发器做一些聪明的事情。)
因此,如果您要使用Doctrine或Propel NestedSet行为,则可以定义表格:
name
(varchar,记录元素名称,例如customer
)is_persistent
(布尔)table_name
(varchar)primary_key
(整数)现在,我们在这里出现了一个有趣的属性:在创建层次结构时,您将看到叶节点中的平凡值可以通过我们的参考系统共享。事实上,我并不完全确定is_persistent
布尔是必需的:它是持久的(如果我已经正确地理解了你的术语),由于共享外部表行,而非持久性,如果它没有。
因此,如果customer1.address.postcode
具有特定字符串值,则可以customer2.address.postcode
指向相同的内容。更新第一个表达式指向的版本时,第二个表达式将自动更新&#34; (因为它解析为同一个表行)。
这里的优势在于,它可以在没有太多工作的情况下进入Propel和Doctrine,并且根本没有任何核心攻击。您需要做一些工作来将对象/数组转换为层次结构,但这可能不是很多代码。
附录:让我解释一下与嵌套元素存储有关的想法。你说你相信你需要在不同的地方分享不同层次的等级,但我不太确定(现在我认为你需要一些鼓励,不要建立一个过于复杂的系统!)。让我们看一个拥有最喜欢的书的用户的例子。
要存储它,我们创建这些层次结构:
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
&#34;持续&#34; (即共享)然后它应该自动共享user.favouriteBook.author
。这是因为如果两个或更多人喜欢同一本书,那么所有人都会是同一个作者。
但是,我在评论中注意到为什么我认为我的显式方法更好:替代方案可能是嵌套集的嵌套集,这可能会变得太复杂,而且我还没有想到你&#39 ;证明你需要那个。权衡的是我的方法需要更多的存储空间,但我认为这很好。你还有一些对象的设置,但是如果你有一个单独的工厂,并且坚固地进行单元测试,我认为你不必担心。
(我认为我的方法也可能更快,但如果不为两者开发原型并测量真实数据集的性能,就更难说了。)
附录2,清理一些评论讨论并将其作为问题背景下的答案予以保留。
要确定我在此提出的建议是否可行,您需要创建原型。我建议使用现有的嵌套集解决方案,例如使用NestedSetBehaviour的Propel,尽管GitHub将有许多其他库可以尝试。在此阶段不要尝试将此原型集成到您自己的ORM中,因为集成工作只会让您分心。目前你想测试可行性的想法,这就是全部。
答案 1 :(得分:0)
我正在从头开始构建一个PHP OOP框架(我别无选择 在这件事上)
你总是有一个选择。
最需要框架来处理面向对象的数据 有效的方式。它不需要快速闪电,它只是 需要成为问题的最佳解决方案
我个人会选择序列化字符串或ORM + MySQL(InnoDB)
对象非常类似于类实例,因为它们是一个 特定类的实例,其中包含一组严格的 属性。
听起来像...对象的定义。因为object是类的实例,所以它必须类似于类结构。此外,class' instance
和object
是一回事。所以你有点说Objects ... resemble objects
。
对象属性可以是基本类型(字符串,数字,bools),但可以 也可以是一个对象实例,也可以是一个对象数组(带有 限制数组必须是相同类型的对象)
是的,这是面向对象编程的目的,也是其中一个强大的功能。