为什么使用ORM和抽象层
数据库是关系型的,PHP5和symfony是面向对象的,为了更有效地使用面向对象方式访问关系的数据库系统,将面向对象的逻辑转换到关系型逻辑是必须的。
ORM的最大优点是重用,数据对象方法可以在应用的不同部分调用,甚至不同的应用中被调用。ORM层同时也封装了数据逻辑——比如,计算论坛用户的等级需要用户发布了多少信息以及发布信息的受欢迎度等等,当要在页面显示用户等级的时候只需调用一个数据模型方法即可,所有复杂的逻辑业务都在方法内部实现。而且,你可以在必要的时候修改计算方式,这是只要修改数据模型方法即可,显示部分根本无需修改。
用对象替代记录,用类替代表还有另一个好处:对象提供的访问属性可以不与表字段对应。看下面的代码:
public function getName() { return $this->getFirstName.' '.$this->getLastName(); } |
所有重复数据存储方法和数据自身的业务逻辑都可以保存在对象中。看个购物车统计:
public function getTotal() { $total = 0; foreach ($this->getItems() as $item) { $total += $item->getPrice() * $item->getQuantity(); }
return $total; } |
还有一个好处就是不同数据库使用的SQL语法可能会有区别,从一种数据库转换到另一种数据库将必须重写SQL查询语句(以前方式),采用抽象层与ORM将不涉及具体的SQL语句实现,只关注操作的业务本身。
Symfony使用Propel作为ORM,Propel使用Creole作为数据库抽象,他们都是第三方组件,都有Propel小组开发。
Symfony项目中所有应用共享一个模型。模型是独立于应用的,模型文件存储在项目根下的lib/model目录。
Symfony数据库大纲
要创建数据对象模型,你不惜告诉symfony如何去影响,即需要给定一个大纲文件。在大纲文件中你定义表、字段、关系、默认值、字符集等信息。
Symfony的大纲文件采用YAML格式,必须保存在myproject/config/文件夹,名字一般为schema.yml。大纲文件也可是使用XML格式,要使用XML文件必须删除YAML文件。
大纲示例
schema.yml |
propel: post _attributes: { phpName: Post } id: title: varchar(255) author_id: body: longvarchar created_at: author: _attributes: { phpName: Author } id: name: varchar(255) password: varchar(255) fullname: varchar(255) |
Schema.xml |
<?xml version="1.0" encoding="UTF-8"?> <database name="propel" defaultIdMethod="native" noxsd="true"> <table name="posts" phpName="Post"> <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" /> <column name="title" type="varchar" size="255" required="true" /> <column name="author_id" type="integer" /> <foreign-key foreignTable="users"> <reference local="author_id" foreign="id"/> </foreign-key> <column name="body" type="longvarchar" /> <column name="created_at" type="timestamp" /> </table>
<table name="author" phpName=" Author "> <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" /> <column name="name" type="varchar" size="50" required="true" /> <column name="password" type="varchar" size="50" required="true" /> <column name="fullname" type="varchar" size="100" /> </table> |
数据库名称blog不会在schema.yml文件中显示,它定义在其他文件(database.yml和propel.ini),这有好处:通过简单的修改数据库链接即可满足不同目的的使用,比如开发过程中使用blog_dev数据库,测试阶段使用blog_test数据库,运行时候使用blog数据库等等。
基本大纲语法
Schema.yml威尔建中第一个键表示连接名。大纲中包含多个表,每个表包含一组列。在YAML中,键以冒号结束,结构以缩进反应(切记不要使用tab)。表可以包含特殊属性,比如phpName(生成后的类名),如果没有定义phpName,symfony使用camelCase方式定义之。
【cameCase约定删除单词中的下划线并将单词的首字母大写,比如:post表的类名为Post,blog_article的类名为BlogArticle】
表包含列信息,列值可通过三种不同方式定义:
l 如果没有定义任何信息,symfony将依据列名和空列约定猜测最佳属性。例如,id列将被定义为自动增长的整型,且设置为主键;post表中的author_id将理解为author表的外间;created_at自动设置为时间戳类型。
l 如果只定义了一个属性则为列的类型。可接受的类型:boolean, integer, float, date, varchar(size), longvarchar (在MySQL中转化为text)等等,如果varchar长度大于255,你应该使用longvarchar类型,longchar类型在MySQL中最大65K;时间与时间戳受限于Unix,不能保存早于 1970-01-01 的值,如果要保存早于这个日期的时间可以采用bu_date和bu_timestamp类型。【bu=before unix】
l 如果要定义其他属性(如:默认值、非空等),你需要使用键值对来些,后面会介绍。
列也可以有phpName属性,采用首字母大写方式(Id,Title,Content等),无需覆盖。
模型类
Schema用来建立ORM层的模型类。在命令行可以非常快速的生成:
symfony propel-build-model |
运行命令后将启动schema分析并在lib/model/om目录生成基本的数据模型类。你会发现,两个表在om目录下产生了四个文件,另外在model目录下还有额外的四个文件,这没有错。
基类与自定义类
为什么在两个目录保留两个版本的数据对象模型?
你可能需要在模型对象中添加自定义方法和属性,但在项目开发过程中,你可能也要调整表以及表结构。一旦修改了schema.yml文件,你需要使用上面的命令行语句重新生成对象模型类,如果你的自定义方法和属性写在了生成的类中,他们将被重新生成所删除!
基类存在lib/model/om目录,是有schema直接生成的。你不应该修改他们。另一方面,自定义类保存在了lib/model目录,他们继承自基类。当调用命令行语句重新生成数据对象模型的时候,他们不会被改动,所以应该将自定义方法和属性放这里。
对象类与Peer类
Post和author是对象类,他们表示了数据库中的记录,提供了对记录及相关记录字段的访问,也就是说可以通过调用Post对象的方法返回一条Post记录的标题。
$post = new Post(); ... $title = $psot->getTitle(); |
PostPeer和AuthorPeer是peer类,类中包含了操作表的静态方法。他们提供了从表获取记录的方法,返回对象或者对象集合。
$posts = PostPeer::retrieveByPks(array(123, 124, 125)); // $posts是Post类的对象数组 |
访问数据
首先让我们看看关系与面向对象的对应表
关系 | 面向对象 |
表 | 类 |
行、记录 | 对象 |
字段、列 | 属性 |
获取列值
通过对象的setter与getter方法管理列值:
$post = new Post(); $post->setTitle('My first article'); $post->setBody('This is my very first article./n Hope you enjoy it!');
$title = $post->getTitle(); $body = $post->getContent(); |
一次设置多个字段需要使用fromArray()方法:
$post->fromArray(array( 'title' => 'My first article', 'body' => 'This is my very first article./n Hope you enjoy it!', )); |
获取关联记录
post表的Author_id列被定义成了author表的外键。每个post关联到一个author,每个author可以有多个post。生成的类包含了5个处理这种关系的方法。
l $post->getAuthor():获取关联的作者对象
l $ post ->getAuthorId():获取关联的作者ID
l $ post ->setAuthor ($article):定义关联的作者对象
l $ post ->setAuthorId($id):定义关联的作者ID
l $Author->getPosts():获取关联的文章对象(多个)
外键的特殊setter方法:
$post = new Post(); $post ->setAuthor('Steve'); $post ->setBody('Gee, dude, you rock: best post ever!');
// 把这个文章关联到$author对象 $post ->setAuthor($author);
//或者如果对象已经存入数据库 $post ->setAuthorId($author->getId()); |
外键的特殊getter方法:
// 多对一关系 echo $post->getAuthor()->getName(); => Oliver echo $post->getAuthor()->getFullname(); => OliverZhang
// 一对多关系 $posts = $author->getPosts(); |
getAuthor()方法返回Author类的对象,可以很方便的获取Name,这比自己链接要好。
$posts变量包含了Post类的对象数组,可以通过$posts[0]或者迭代地获取每一个Post对象:foreach($posts as $post)。
表对象最好使用单数,否则,如果使用复数,比如Posts作为文章表的php对象,外键关联产生的方法中会出现getPostss(),这很难看!!
保存和删除
通过调用构造方法,你创建了新的对象,但却不是表中的新记录。修改对象不会更改到数据库。要保存数据,需要调用对象的save()方法。
$article->save(); |
ORM很只能,他能探测出对象之间的关联关系,所以,保存$author对象同时也保存了关联的$post对象。而且ORM会自动判断是INSERT还是UPDATE。主键也会自动被save()方法设置,也就是说,如果调用了save()方法就可以使用$author->getID()或取得新插入数据库的作者ID。调用isNew()方法判断对象是否新加入的,调用isModified()方法来查看对象是否修改过,如果必要可以调用保存方法。
删除用户的文章可以使用下面的代码:
foreach ($article->getComments() as $comment) { $comment->delete(); } |
即使在调用了delete()方法,对象在请求结束前仍保留着,看看一个对象是否在数据库被删除可以调用isDeleted()方法。
通过主键获取记录
如果知道记录的主键值,使用peer类的retrieveByPk()方法来获取对象:
$post = PostPeer::retrieveByPk(7); |
有时主键可能有多个列组成,这是,retrieveByPk方法可以接受多参数,每一个对以一个主键列。如果想通过主键获取多条记录使用retrieveByPks()方法。
使用规则(Criteria:标准?)获取记录
如果要获取多条记录需要调用peer类的doSelect()方法,比如要获取Author类的对象调用AuthorPeer::doSelect()方法。方法的第一个参数是Criteria类的对象,为数据库抽象定义的不包含SQL的简单查询定义。空的Criteria返回类中所有的对象,相当于没有限定条件(没有SQL语句中的where部分):
$c = new Criteria(); $articles = ArticlePeer::doSelect($c);
// 相当于下面的SQL语句 SELECT post.ID, post.TITLE, post.BODY, post.CREATED_AT FROM post; |
Hydrating |
调用::doSelect()方法实际上比简单SQL查询要强大的多,SQl只使用你选择的DBMS;而且传递给Criteria的任何值在集成到SQL代码后都经过了避让,这完全防止了注入危险;第三,方法返回了对象数组,要比数据集好。 |
对于更复杂的查询,你可能需要类似WHER、Order By、Group by以及其他SQL语句。Criteria对象有对应的方法和参数。例如,要获取所有POSTS——作者为Oliver,按时间排序,可以使用下面的代码:
$c = new Criteria(); $c->add(PostPeer::AUTHOR, 'Oliver'); $c->addAscendingOrderByColumn(PostPeer::CREATED_AT); $Posts = PostPeer::doSelect($c); //将得到类似如下的代码 SELECT post.ARTICLE_ID, post.AUTHOR, post.BODY, post.CREATED_AT FROM post WHERE post.author = 'Oliver' ORDER BY post.CREATED_AT ASC; |
类常量作为参数传递给add()方法,他们使用大写格式,例如要加入post表的body列,使用PostPeer::BODY类常量。
SQL与Criteria对象的映射关系:
SQL | Criteria |
WHERE column = value | ->add(column, value); |
WHERE column <> value | ->add(column, value, Criteria::NOT_EQUAL); |
Other Comparison Operators |
|
> , < | Criteria::GREATER_THAN, Criteria::LESS_THAN |
>=, <= | Criteria::GREATER_EQUAL, Criteria::LESS_EQUAL |
IS NULL, IS NOT NULL | Criteria::ISNULL, Criteria::ISNOTNULL |
LIKE, ILIKE | Criteria::LIKE, Criteria::ILIKE |
IN, NOT IN | Criteria::IN, Criteria::NOT_IN |
Other SQL Keywords |
|
ORDER BY column ASC | ->addAscendingOrderByColumn(column); |
ORDER BY column DESC | ->addDescendingOrderByColumn(column); |
LIMIT limit | ->setLimit(limit) |
OFFSET offset | ->setOffset(offset) |
FROM table1, table2 WHERE table1.col1 = table2.col2 | ->addJoin(col1, col2) |
FROM table1 LEFT JOIN table2 ON table1.col1 = table2.col2 | ->addJoin(col1, col2, Criteria::LEFT_JOIN) |
FROM table1 RIGHT JOIN table2 ON table1.col1 = table2.col2 | ->addJoin(col1, col2, Criteria::RIGHT_JOIN) |
查看有哪些方法可以使用和如何使用,可以在lib/model/om目录下查看对应的类文件,方法与属性都包含了注释,但不详细,要想得到详细的注释,打开config/propel.ini文件中propel.builder.addComments参数。
看个更复杂的例子:
$c = new Criteria(); $c->add(PostPeer::AUTHOR, 'Oliver'); $c->addJoin(PostPeer::AUTHOR_ID, AuthorPeer::ID); $c->add(AuthorPeer::BODY, '%enjoy%', Criteria::LIKE); $c->addAscendingOrderByColumn(PostPeer::CREATED_AT); $posts = PostPeer::doSelect($c); |
虽然SQL是简单的语言,但你却能够建立复杂的查询,Criteria对象能够处理各种复杂的条件。但很多人是根据SQL语句来设置Criteria,呵呵,反了!所以初期最好的方式是使用例子来做自己的Criteria。Symfony项目的官网有很多Criteria的例子可供参考。
另外,每一个peer类都有一个doCount()方法,他只是简单的计算满足Criteria的记录条数并返回一个整数。应为没有对象返回,所以hydrating处理过程不会发生。doCount()方法要比doSelect()方法快。
Peer类同样提供了doDelete() doInsert() doUpdate()方法,他们都需要一个Criteria参数。这些方法处理你的DELETE、INSERT和UPDATE操作。
最后,如果你指向获取第一个返回对象,使用doSelectOne()方法。
当doSelect()方法返回的结果数目很大,你可能希望只显示其中的以部分,Symfony提供了sfPropelPager分页方法类,使用他可以自动对结果进行分页。
使用原始(Raw)SQL查询
不使用ORM方式查询,具体步骤如下:
l 获取数据库链接
l 建立查询字符串
l 在外面建立一个声明(Create a statement out of it.)
l 迭代结果集(Iterate on the result set that results from the statement execution.)
还是看看代码吧:
$connection = Propel::getConnection(); //获取数据库链接 $query = 'SELECT MAX(%s) AS max FROM %s'; //建立查询字符串(预处理) $query = sprintf($query, ArticlePeer::CREATED_AT, ArticlePeer::TABLE_NAME); //填充 $statement = $connnection->prepareStatement($query); //调用数据库链接的预处理声明 $resultset = $statement->executeQuery(); //执行查询 $resultset->next(); //获取结果 $max = $resultset->getInt('max'); |
使用这种方式要进行必要的代码检验,防止注入!!
使用特殊的时间列
通常,表中的created_at字段保存记录创建时候的时间戳,还有updated_at列,他保存了最后的更新时间。这些都是自动完成的,不需要任何操作。
数据库连接
数据模型对立与使用的数据库,symfony中设置数据库在config目录de databases.yml文件中:myproject/config/databases.yml
prod: propel: param: host: mydataserver username: myusername password: xxxxxxxxxx
all: propel: class: sfPropelDatabase param: //可以简单的使用 dns: http://***** # 数据库类型,可选项:Mysql、sqlserver、pgsql、sqlite、oracle phptype: mysql hostspec: localhost database: blog username: login password: passwd port: 80 encoding: utf8 # 创建时的默认字符集 persistent: true # 使用持久链接 |
连接设置是针对环境的,可以给不同的环境定义不同的连接设置。这些设置可以被应用下的设置覆盖(应用下默认没有databases.yml文件,可以建立,而后覆盖项目的设置)
如果使用了SQLite,hostspec参数必须设定为数据库文件的路径。例如,如果数据库文件在data/forum.db,则连接设置为:
all: propel: class: sfPropelDatabase param: phptype: sqlite database: %SF_DATA_DIR%/blog.db |
扩展模型
模型生成方法虽然强大但却不够丰富。一旦你要观测自己的业务逻辑,就需要扩展他,或者添加新方法或者覆盖现有方法。
添加新方法
添加的新方法要放在lib/model/目录下对应的文件中(不要放在om下,具体原因前面已经解释)。使用$this来调用当前对象的方法,使用sef::来调用当前类的静态方法。记住,自定义类继承自基类。
例如添加Post的魔法方法__toString()来回显类对象的标题:lib/model/Article.php
<?php
class Post extends BasePost { public function __toString() { return $this->getTitle(); // getTitle()方法继承自BasePost } } |
扩展peer类,例如添加方法获取所有的posts并以创建时间排序,代码如下:
lib/model/ArticlePeer.php
<?php
class PostPeer extends BasePostPeer { public static function getAllOrderedByDate() { $c = new Criteria(); $c->addAscendingOrderByColumn(self:CREATED_AT); return self::doSelect($c); //静态方法的调用
} } |
添加的方法与生成的代码一样使用。
覆盖现有方法
如果基类中生成的方法不满足你的需求,你可以在自定义类中覆盖他,要记住必须使用同样的方法标识(同样的方法名和同样的参数列表)。具体实现与添加方法一样。
使用模型行为
一些模型的修正方法可能会使用的很多的类上,symfony打包这些修正扩展方法为模型行为。魔性行为提供额外模型类方法的是扩展类,模型类已经包含了hooks并且symfony知道如何使用sfMixer扩展他们。
要激活行为,必须修改config目录下的propel.ini文件:
propel.builder.AddBehaviors = true // 默认为false |
在symfony中默认没有绑定模型行为,但可以通过安装插件来实现。一旦安装了插件,就可以通过一句话把模型行为分配给类。例如,已经安装了sfPropelParanoidBehaviorPlugin插件,你可以在Post.class.php文件结尾处使用下面的语句扩展Post类:???????????
这样,重建模型,删除Post对象将不会在数据库中删除,只是在ORM中不可见,除非你临时使用sfPropelParanoidBehavior::disable()禁用行为。
扩展大纲(schema)语法
Schema.yml文件可以很简单,但关系模型经常很复杂,schema有丰富的语法来处理几乎所有情况。
属性
链接和数据包有许多特殊属性,如下代码:
propel: _attributes: { noXsd: false, defaultIdMethod: none, package: lib.model } Post: _attributes: { phpName: Post } |
要使schema在起效之前被检验,需要在关闭noXSD属性。defaultIdMethod属性对MySQL的自动增长起效。Package属性就像命名空间,他决定了生成的类放在何处,默认是lib/model。
包含区域内容的表(即有几种版本的内容,存储在关系表,为国际化设计)有额外的两个属性,看代码:i18n表属性
propel: post: _attributes: { isI18N: true, i18nTable: db_group_i18n } |
使用多大纲 |
一个应用中可能有多个大纲,symfony将会考虑config目录下每一个以schema.yml或schema.xml结束的文件如果你的应用中欧牛国友很多表,或者一些表不共享同样的链接,你会发现郑重方式非常有用。 |
列细节
看个较为详细的例子
propel: post: id: { type: integer, required: true,primaryKey: true, autoIncrement: true } name: { type: varchar(50), default: foobar, index: true } group_id: { type: integer, foreignTable: db_group,foreignReference: id, onDelete: cascade } |
列参数详解:
l type: 列类型. 可选项有boolean, tinyint, smallint, integer, bigint, double, float, real, decimal, char, varchar(size), longvarchar, date, time, timestamp, bu_date, bu_timestamp, blob, 和 clob.
l required: Boolean. 是否必须
l default: 默认值.
l primaryKey: Boolean. 是否主键.
l autoIncrement: Boolean. 是否自动增长.
l sequence: (PostgreSQL 和 Oracle中会用到).
l index: Boolean. 是否创建索引(true或者unique)
l foreignTable: 一个表名, 使用一个另外表的字段作为外键
l foreignReference: 外键名称
l onDelete: 删除时的操作,(setnull或者cascade)。Setnull表示将外键设置为空,cascade表示级联删除记录——删除包含外键的记录。如果数据库系统不支持,ORM会模拟执行
l isCulture: Boolean. 是否为culture列
外键
另一种设置外键的方式:
propel: post: id: title: varchar(50) author_id: { type: integer } _foreignKeys: - foreignTable: author onDelete: cascade references: - { local: author_id, foreign: id } |
索引
另一种设置索引的方式:
propel: post: id: title: varchar(50) created_at: _indexes: my_index: [title, author_id] _uniques: my_other_index: [created_at] |
对于在多个列上创建索引,这种方式非常有用
空列
Symfony对待空列有不同的方式,具体如下:
l
命名为id的空列被认为是主键且自动增长
id: { type: integer, required: true, primaryKey: true, autoIncrement: true }
l
命名为XXX_id的列被认为是外键
author_id: { type: integer, foreignTable: db_foobar, foreignReference: id }
l
命名为created_at、updated_at、created_on和updated_on的列被认为是时间戳类型
created_at: { type: timestamp }
对于外键,symfony将去讯在具有phpName为XXX的表,如果找到则设置为外见表。
i18n表
symfony支持内容国际化。也就是说,需要国际化的内容会被存储在两个分割的表中:一个有不变的列,另一个使用国际化列。在schema.yml文件中如果定义了以下的代码,则symfony自动完成列以及表属性的国际化:
propel: db_group: id: created_at:
db_group_i18n: name: varchar(50)
//类似于 propel: db_group: _attributes: { isI18N: true, i18nTable: db_group_i18n } id: created_at:
db_group_i18n: id: { type: integer, required: true, primaryKey: true,foreignTable: db_group, foreignReference: id, onDelete: cascade } culture: { isCulture: true, type: varchar(7), required: true,primaryKey: true } name: varchar(50) |
使用schema.xml
前面好像有例子,这里就不说了,实际上YAML书写上方便的多。
不要创建模型两次
从现有schema创建数据库结构
Symfony propel-build-sql //创建sql脚本 |
Symfony propel-insert-sql //创建数据库结构,可以使用其他方式,但这是最方便的 |
从现有数据库生成YAML格式的数据模型
首先要保证propel.ini文件中设置了正确的数据库以及连接的设置信息。
Symfony propel-build-schema //创建schema.yml文件 |
Symfony propel-build-schema xml //创建schema.xml文件 |
注意一下propel.ini配置文件,大多数的选项不需用户干涉,除了下面的:
基类将自动加载,如果不希望这样,而是使用include_once语句则
propel.builder.addIncludes = true |
生成的类默认包含很少的注释,不过可以改变
propel.builder.addComments = true |
模型行为默认不处理,修改下面代码改变之
propel.builder.AddBehaviors = true |
Propel.ini一旦修改,记得要重新生成模型以让变动生效。