第08章深入模型层.docx
- 文档编号:15493061
- 上传时间:2023-07-05
- 格式:DOCX
- 页数:35
- 大小:41.33KB
第08章深入模型层.docx
《第08章深入模型层.docx》由会员分享,可在线阅读,更多相关《第08章深入模型层.docx(35页珍藏版)》请在冰点文库上搜索。
第08章深入模型层
第8章-深入模型层
到目前为止本书大多数的讨论都专注于建立页面,处理请求与回应。
但是网页应用程序的业务逻辑大多依赖于它的数据模型。
symfony的默认模型组件是基于一个对象/关系映射层,也就是我们所知的Propel项目(http:
//propel.phpdb.org/)。
在symfony应用程序中,你不用关注数据库的实际位置,而是通过对象来访问储存在数据库中的数据的。
这保持了高度的抽象和可移植性。
本章解释了如何建立一个数据对象模型和如何通过Propel来访问和修改数据。
同时也展示了symfony是如何整合Propel的。
为什么使用ORM和抽象层?
数据库是关系型的。
PHP5和symfony都是面向对象的。
为了在面向对象环境中最有效的访问数据库,需要一个接口用来把对象逻辑转换为关系逻辑。
如第一章所述,这个接口就叫做对象-关系映射(ORM),它是由可以访问数据和保持业务规则的对象组成的。
ORM最大的优势就是可重用性,它允许数据对象的方法可以被应用程序的其他部分调用,甚至可以从不同的应用程序中调用。
ORM层也可以封装数据逻辑,例如,基于用户的贡献度和如何作出的贡献来计算论坛用户的评分。
当一个页面需要显示例如一个用户的评分,不需要担心如何去计算,只要很简单的调用数据模型的方法即可。
如果以后计算方法有所变化,你只需要修改模型的评分方法即可,应用程序的其他部分不需要改变。
使用对象来代替记录,用类来代替表,还有其他好处:
他们允许你在对象中增加一个新的读取方法而不需要对应到表的一个列。
例如,如果你有一个client表,它拥有两个字段分别叫做first_name和last_name,你可能只想要获得一个Name。
在面向对象的世界里,Client类中增加一个存取方法是非常简单的,如例8-1所示。
从应用程序的角度来看,Client类的FirstName,LastName和Name属性没有什么区别。
只有类本身才能决定属性所对应的数据库的列。
例8-1-在模型类中的存取方法掩盖了实际表结构
publicfunctiongetName()
{
return$this->getFirstName().''.$this->getLastName();
}
所有重复的数据访问函数和数据自身的业务逻辑可以存在对象中。
假设你有一个ShoppingCart类,里面有一个Item(是个对象)。
只要写一个自定义的方法来封装实际的计算过程,就可以在结账时得到购物车的总价。
如例8-2所示。
例8-2-存取方法掩盖了数据逻辑
publicfunctiongetTotal()
{
$total=0;
foreach($this->getItems()as$item)
{
$total+=$item->getPrice()*$item->getQuantity();
}
return$total;
}
在建立数据访问过程的时候还要考虑另外一个要点:
数据库厂商所使用的不同的SQL语法变种。
换用另外一个数据库管理系统(DBMS)会让你不得不重写一部分为以前设计的SQL查询。
如果用数据库独立语法来建立一个查询,并把实际SQL翻译为第三方语言,换数据库系统就不会麻烦了。
这就是数据抽象层存在的目的。
它强制让你使用特定的语法规则来写查询,同时把它转到到相应的DBMS并优化SQL语句。
抽象层的主要优势是可移植性,因为他让换用另一种数据库成为可能,甚至可以在项目中期换用。
假设你需要为应用程序写一个快速原型,但客户还没有确定哪个数据库最适合他。
你能先用SQLite建立你的应用程序,然后在客户有了决定后切换到MySQL,PostgreSQL或者Oracle。
这只要改变配置文件中一行代码即可。
symfony使用Propel来实现ORM,Propel使用Creole做数据库抽象。
这两个第三方组件,都是由Propel小组开发的,并且都无缝集合到了symfony中,你可以把他们作为框架的一部分。
在本章描述的Propel和Creole的约定和语法规则都被改写过,因此symfony的语法与原始语法会有一些不同。
NOTE在symfony项目中,所有的应用程序共享同一个模型。
这就是项目层的全局观:
依靠通用商业规则重组应用程序。
这就是让模型独立于应用程序之外并且模型文件存在项目根目录的lib/model/目录下的原因。
symfony的数据库设计(schema)
为了创建symfony使用的数据对象模型,需要把数据库关系模型翻译为对象数据模型。
ORM需要关系模型的描述来做映射,这就叫做设计。
在设计中,你定义表,表之间的关系,和表中列的特性。
symfony中设计的语法使用了YAML格式。
schema.yml文件必须放在myproject/config/目录下。
NOTEsymfony也接受Propel原生的XML设计格式,在本章稍后的超越schema.yml:
schema.xml小节会做描述。
设计示例
如何把数据库结构转换为设计呢?
看例子是最好的理解方法。
试想一下你有一个blog数据库,包含两个表blog_article和blog_comment,结构如图8-1所示。
图8-1-一个blog数据库表结构
对应的schema.yml文件应该看上去如例8-3所示。
例8-3-schema.yml示例
propel:
blog_article:
_attributes:
{phpName:
Article}
id:
title:
varchar(255)
content:
longvarchar
created_at:
blog_comment:
_attributes:
{phpName:
Comment}
id:
Mutator
article_id:
author:
varchar(255)
content:
longvarchar
created_at:
注意数据库名字本身(blog)并没有出现在schemal.yml文件中。
反而在连接名下有数据库描述(本例中是propel)。
这是因为实际的连接设置可以依照应用程序运行的环境来定。
例如,当你在开发环境中运行应用程序时,你会访问一个开发数据库(也许是blog_dev),但是生产数据库也用的是同一个设计文件。
在database.yml文件中有连接,本章稍后的"数据库连接"会有介绍。
设计不包含任何连接设置的细节,只有连接名用来保持数据库抽象。
基本设计语法
在schema.yml文件中,第一个关键字表示的是连接名。
它可以包含多个表,每个表都有一些列。
根据YAML语法,关键字用冒号作结束标记,结构用缩进来表示(一个或多个空格,但不是制表符)。
表可以有特殊的属性,包括phpName(会生成同名的类)。
如果没有设置表的phpName,symfony会以表名的驼峰命名法来创建类。
TIP驼峰命名法把单词之间的下划线去掉了,并把每个单词的首字母大写。
blog_article和blog_comment的默认驼峰命名法版本是BlogArticle和BlogComment。
这种转换方法约定字的首字母大写,就像骆驼的驼峰一样。
一个表包含了许多列。
列的值可以用三种方式来定义:
∙如果你没有给出定义,symfony会根据列名和一些约定来猜测最适合的属性,这些在下面的"空列"段落会有描述。
例如,在例8-3中的id列不需要定义。
symfony会定义它为自增长的数值类型,表的主键。
blog_comment表的article_id会理解为blog_article表的外键(结尾是_id的列被理解为外键,根据列前面部分的名字来确认是和哪张表有关联)。
create_at列会自动设置为timestamp类型。
对于这种类型的列,你不需要特别指定他们的类型。
这就是为什么说schema.yml是非常容易写的原因。
∙如果你只定义了一个属性,那这就是列的类型。
symfony支持一些常用的列类型:
boolean,integer,float,date,varchar(size),longvarchar(转换过的,例如,在MySQL下就会转换为text)和其他。
对于text内容超过256个字符的,需要使用longvarchar类型,这是没有大小限制的(MySQL中不能超过65KB)。
注意date和timestamp类型有通常的Unix日期限制并且不能设置早于1970-01-01。
如果需要设置一个更早的日期(例如,生日),“unix日期之前”的日期类型可以使用bu_date和bu_timestamp。
∙如果需要定义其他的列属性(如默认值,必填属性或者其他),你需要把列属性写为一组key:
value。
这种扩充设计语法会在本章稍后介绍。
列可以有一个phpName属性,它是首字母大写的(Id,Title,Content,等)并且在大多数情况下不能覆盖。
表也可以包含详细的外键和索引,以及少量的数据库结构定义。
参考本章节后面的“扩展设计语法”部分。
模型类
设计是用来建立ORM层的模型类的。
为了省时,这些类是通过命令行调用propel-build-model来生成的。
>symfonypropel-build-model
输入这个命令后会先分析模型接着在项目的lib/model/om目录下生成基础数据模型类:
∙BaseArticle.php
∙BaseArticlePeer.php
∙BaseComment.php
∙BaseCommentPeer.php
还有,实际的数据模型类会建立在lib/model下:
∙Article.php
∙ArticlePeer.php
∙Comment.php
∙CommentPeer.php
你只定义了两个表,但会生成八个文件。
这并无不妥,但应该解释一下。
基础类和自定义类
为什么我们在两个不同的目录保留了两个版本的数据对象模型?
你也许会需要在模型对象中增加自定义方法和属性(试想例8-1中的getName()方法)。
但是由于项目开发需要,你将会需要增加表或者列。
当你修改了schema.yml文件时,需要重新调用propel-build-model来生成对象模型类。
如果你的自定义方法写在自动生成的类中,他们会在每一次重新生成的时候被覆盖。
由设计直接生成的Base类放在lib/model/om/目录中。
你永远不需要去修改他们,因为每一次新建模型都会完全删除这些文件。
另一方面,自定义对象类会放在lib/model/目录下,实际上是继承自Base类。
当对已有的模型调用propel-build-model任务时,这些类不会被修改。
因此这就是你可以增加自定义方法的地方。
例8-4展示了第一次调用propel-build-model任务建立的自定义模型类的一个示例。
例8-4-lib/model/Article.php中的模型类文件示例
php
classArticleextendsBaseArticle
{
}
它继承了BaseArticle类所有的方法,但是修改设计不会影响到这个文件。
用自定义类来扩展基础类的机制可以让你在不知道最终数据库中模型之间的关系的时候开始编程。
相关的文件结构会让模型既可以自定义又可以进化。
对象和Peer类
Article和Comment是用来显示数据库中记录的对象类。
他们赋予了记录的列和相关记录的访问权限。
这就是说你可以调用Article对象的方法来获取文章的标题,如例8-5所示。
例8-5-在对象类中获得记录列
$article=newArticle();
...
$title=$article->getTitle();
ArticlePeer和CommentPeer都是peer类;因此,类包含了静态方法来操作表。
他们提供了从表中获得记录的方法。
他们的方法通常返回了一个对象或是相关对象类的对象的集合,如例8-6所示。
例8-6-Peer类可以用静态方法来获得记录
$articles=ArticlePeer:
:
retrieveByPks(array(123,124,125));
//$articles是一个Article类的对象数组
NOTE从数据模型的角度来看,不可能有peer对象。
这就是为什么调用peer类的方法会使用:
:
(调用静态方法)而不是通常的->(调用实例方法)。
所以把对象类和peer类的基础类和自定义类加起来,数据库设计里的一个表会自动生成四个类。
事实上,有第五种类生成在lib/model/map/目录下,它们包含了关于表运行时所需要的metadata信息。
但是也许你永远不需要修改这个类,你完全可以忘了它。
访问数据
在symfony中,是通过对象来访问数据的。
如果你习惯使用关系模型和使用SQL来获取、修改你的数据的话,对象模型方法会让你觉得有些复杂。
但是当你尝试过用面向对象方法来访问数据的话,就会喜欢上它的。
但是首先,让我们确信我们说的是同一个词汇。
关系型和对象数据模型有一些相似点,但是他们都有自己的术语:
关系的
面向对象的
表
类
行,记录
对象
字段,列
属性
获得列值
当symfony建立模型时,它为每一个在schema.yml中存在的表都建立一个基础对象类。
每一个类都有一个基于列定义的默认的构造器,读取方法和设置方法:
new,getXXX()和setXXX()方法帮助创建对象并给予访问对象属性的权限,如例8-7所示。
例8-7-生成对象类方法
$article=newArticle();
$article->setTitle('Myfirstarticle');
$article->setContent('Thisismyveryfirstarticle.\nHopeyouenjoyit!
');
$title=$article->getTitle();
$content=$article->getContent();
NOTE生成的对象类名为Article,这是由blog_article表的phpName定义的。
如果在设计中没有定义phpName,这个类就会取名为BlogArticle。
读取方法和设置方法使用驼峰命名法的变异来定义列名,所以getTitle()方法会获得title列的值。
可以使用fromArray()方法一次定义多个字段,在生成的每个类对象中都有此方法,如例8-8所示。
例8-8-fromArray()方法是一个多重设置方法
$article->fromArray(array(
'title'=>'Myfirstarticle',
'content'=>'Thisismyveryfirstarticle.\nHopeyouenjoyit!
',
));
获得相关联的数据
在blog_comment表中article_id列实际上定义了blog_article表的一个外键。
每一个comment都与一篇文章相对应,同时一篇文章可以有多个comment。
生成的类包含五个方法来把这些对应关系转换成面向对象的方法,如下:
∙$comment->getArticle():
获得相关联的Article对象
∙$comment->getArticleId():
获得相关联的Article对象的ID
∙$comment->setArticle($article):
定义相关联的Article对象
∙$comment->setArticleId($id):
通过ID定义相关联的Article对象
∙$article->getComments():
获得相关联的Comment对象
getArticleId()和setArticleId()方法说明了你可以把article_id列作为一个普通列并手动设置对应关系,但这么做并不好。
面向对象方法的优点让其他三个方法更容易理解。
例8-9显示了如何使用生成的设置方法。
例8-9-外键转换为特别的设置方法
$comment=newComment();
$comment->setAuthor('Steve');
$comment->setContent('Gee,dude,yourock:
bestarticleever!
);
//把此comment和$article对象关联
$comment->setArticle($article);
//另一种语法
//仅当对象已经存在于数据库中才有意义
$comment->setArticleId($article->getId());
例8-10展示了生成的获取方法是如何使用的。
同时也演示了如何在模型对象中调用关联方法。
例8-10-外键转为特别的getters
//多对一关系
echo$comment->getArticle()->getTitle();
=>Myfirstarticle
echo$comment->getArticle()->getContent();
=>Thisismyveryfirstarticle.
Hopeyouenjoyit!
//一对多关系
$comments=$article->getComments();
getArticle()方法返回了一个Article类的对象,从而可以使用getTitle()获取方法。
这比直接使用join要好得多,而仅仅只会多几行代码(从调用$comment->GetArticleId()开始)。
例8-10中的$comments变量包含了Comment类的一个对象数组。
你能用$comments[0]来显示第一个对象或是用foreach($commentsas$comment)来遍历这个对象数组。
NOTE你现在知道为什么模型对象是以单数命名的了。
在Comment对象名字后面增加s,会在blog_comment表中制造一个外键并产生建立getComments()方法的动作。
如果你给模型对象一个复数名字,生成时候会产生一个叫做getCommentss()的无意义的方法。
保存和删除数据
创建一个新的对象可以通过调用new构造器,但修改此对象不会对数据库有任何影响,也就是说这并不对应于blog_article表中存在的实际记录。
但你可以调用对象的save()方法把数据保存到数据库中。
$article->save();
ORM可以查明对象之间的关系,因此保存$article对象同时也就保存了相关的$comment对象。
也就是说它知道保存了的对象在数据库中有关联的数据,当调用save()的时候,有时候会转换为INSERT语句,有时候会使UPDATE语句。
save()方法会自动设置主键,所以在保存后,你能用$article->getId()得到一个新的主键。
TIP你能通过调用isNew()来检查对象是否是新建的。
如果你想知道对象是否被修改过是否该保存,可以调用它的isModified()方法。
如果你读了文章的comment,也许会后悔把他们发布到互联网上。
如果你觉得一些回复者的回复不合适的话,可以很方便的使用delete()方法来删除评论,如例8-11所示。
例8-11-用delete()方法从数据库删除记录的相关对象
foreach($article->getComments()as$comment)
{
$comment->delete();
}
TIP在调用delete()方法后,请求结束之前对象依旧可以访问。
要确认是否在数据库中已经把对象删除的话就需要调用isDeleted()方法了。
通过主键来获得记录
如果你知道特定记录的主键值,可以使用peer类的retrieveByPk()方法来获得相关对象。
$article=ArticlePeer:
:
retrieveByPk(7);
schema.yml文件中定义了id字段作为blog_article表的主键,因此这个语句会返回id为7的文章。
由于你使用了主键,所以只会返回一条记录;$article变量包含了类Article的对象。
有时候,也许包含了多个主键(复合主键)。
在这些情况中,retrieveByPK()方法会接受多个参数,每一个对应一个主键。
你也能用retrieveByPKs()方法,输入一组主键组成的数组作为参数来获得多个对象。
通过Criteria获得数据
当你想获得多个记录时,你需要调用peer类的doSelect()方法来获得你想要的对象。
例如,调用ArticlePeer:
:
doSelect()来获得Article类的对象。
doSelect()方法的第一个参数是Criteria类的一个对象,Criteria类是一个简单查询定义类,它为了用数据库抽象而没有使用SQL。
一个空的Criteria返回了类的所有对象。
例如,例8-12的代码就返回了所有的article。
例8-12-通过Criteria的doSelect()来获得数据--空的Criteria
$c=newCriteria();
$articles=ArticlePeer:
:
doSelect($c);
//和下面SQL查询结果是一样的
SELECTblog_article.ID,blog_article.TITLE,blog_article.CONTENT,
blog_article.CREATED_AT
FROMblog_article;
SIDEBAR化合(hydrating)
调用:
:
doSelect()比使用简单的SQL查询强大的多。
首先,SQL会针对使用的DBMS而优化。
其次,任何传递给Criteria的值都会在整合入SQL代码之前被转义,这能防止SQL注入的风险。
第三点,此方法返回了一个对象数组而不是一个结果集。
ORM基于数据库结果集自动创建并丢出对象。
这个过程叫做化合(hydrating)。
如果遇到一个更复杂的对象选择时,你需要用到WHERE,ORDERBY,GROUPBY和其他SQL语句。
Criteria对象有针对所有这些情况的方法和参数。
例如,在例8-13中我们建立了一个Criteria来取得Steve写的所有的comments,按照日期排序。
例8-13-通过Criteria的doSelect()来获得记录--有条件的Criteria
$c=newCriteria();
$c->add(CommentPeer:
:
AUTHOR,'Steve');
$c->addAscendingOrderByColumn(CommentPeer:
:
CREATED_AT);
$comments=CommentPeer:
:
doSelect($c);
//等同于下面SQL语句执行的结果
SELECTblog_comment.ARTICLE_ID,blog_comment.AUTHOR,blog_comment.CONTENT,
blog_comment.CREATED_AT
FROMblog_comment
WHEREblog_comment.author='Steve'
ORDERBYblog_comment.CREATED_ATASC;
把类常量
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第08章 深入模型层 08 深入 模型