Alan Hou的个人博客

Magento开发系列之六 安装、升级脚本

在任何高效的软件开发项目中,保持开发库和线上库同步都会是一件棘手的事情。Magento通过在系统中创建带有版本号的资源迁移脚本来让这种开发过程更加的顺畅。前面有关ORM的章节我们创建了一个weblog模型,那时我们使用CREATE TABLE语句直接在数据库中创建表格,今天通过对模块的资源配置(Setup Resource)来创建数据表。我们还会在模块中创建升级脚本来完成对已创建模型的升级。主要有以下几步:

  1. 在配置文件中添加Setup资源
  2. 创建资源类文件
  3. 创建安装脚本
  4. 创建升级脚本

我们还是在上一节创建的weblog模块中进行修改,在config.xml的<global />标签内添加以下代码:

<resources>
	<weblog_setup>
		<setup>
			<module>Alanhou_Weblog</module>
			<class>Alanhou_Weblog_Model_Resource_Setup</class>
		</setup>
	</weblog_setup>
</resources>

<weblog_setup>标签用于标识这个安装资源,通常也建议使用modelname_setup的命名机制。<module>Alanhou_Weblog</module>的内容应为模块的命名空间和模块名Packagename_Modulename。<class>标签中包含的是我们将要为Setup资源创建的类名,对于普通的setup脚本可以不自建类,但现在这么做会给以后的开发带来更大的灵活性。

添加完上面的代码后清除缓存并加载Magento中的任意页,会出现如下提示:

Warning: include(Alanhou\Weblog\Model\Resource\Setup.php): failed to open stream: No such file or directory

创建app/code/local/Alanhou/Weblog/Model/Resource/Setup.php并加入如下代码:

<?php
class Alanhou_Weblog_Model_Resource_Setup extends Mage_Core_Model_Resource_Setup{

}

重新访问任意页,前面的报错会消失页面正常显示。

下面我们来创建安装脚本,这个脚本中将包含初始化模块时所需的CREATE TABLE及其它SQL语句。首先让我们来看一下config.xml文件

<config>
	<modules>
		<Alanhou_Weblog>
			<version>0.1.0</version>
		</Alanhou_Weblog>
	</modules>
</config>

这段代码在所有的config.xml文件中都需要添加,主要用于确定模块及版本号。我们的安装脚本就需要用到版本号,本例将假设初使版本号为0.1.0。创建app/code/local/Alanhou/Weblog/sql/weblog_setup/mysql4-install-0.1.0.php文件并加入如下代码:

<?php
echo "Running this upgrade: ".get_class($this)."\n<br />\n";
die("Exit for now");

目录名weblog_setup需要与conifg.xml中的标签<weblog_setup />相同,而文件名称中的0.1.0则应与模块的版本号相同,刷新缓存并访问任意页面会得到如下提示:

Running this upgrade: Alanhou_Weblog_Model_Resource_Setup
Exit for now

注:如果看不到如上提示可能是由于之前访问过页面,执行如下语句再重新访问即可:

delete from core_resource where code = 'weblog_setup';

上面的提示说明升级脚本确实运行了,后面我们会加入SQL升级脚本,不过现在还是先了解下安装机制。删除die()语句,再次访问会在页面的头部出现Running this upgrade: Alanhou_Weblog_Model_Resource_Setup,重复再刷新一下提示就会消失而页面也显示正常。

Magento的安装资源让我们可以轻易地将安装或升级脚本放到服务器上,然后系统自动运行这些脚本,这样系统中所有的数据库迁移脚本都会保持连续性。

使用熟悉的数据库客户端输入如下语句查询core_table数据表:

mysql> select * from core_resource;
+-------------------------+----------------+----------------+
| code                    | version        | data_version   |
+-------------------------+----------------+----------------+
| adminnotification_setup | 1.6.0.0        | 1.6.0.0        |
| admin_setup             | 1.6.1.1        | 1.6.1.1        |
| api2_setup              | 1.0.0.0        | 1.0.0.0        |
| api_setup               | 1.6.0.1        | 1.6.0.1        |
| backup_setup            | 1.6.0.0        | 1.6.0.0        |
| bundle_setup            | 1.6.0.0.1      | 1.6.0.0.1      |
| captcha_setup           | 1.7.0.0.0      | 1.7.0.0.0      |
| catalogindex_setup      | 1.6.0.0        | 1.6.0.0        |
| cataloginventory_setup  | 1.6.0.0.2      | 1.6.0.0.2      |
| catalogrule_setup       | 1.6.0.3        | 1.6.0.3        |
| catalogsearch_setup     | 1.8.2.0        | 1.8.2.0        |
| catalog_setup           | 1.6.0.0.19.1.2 | 1.6.0.0.19.1.2 |
| checkout_setup          | 1.6.0.0        | 1.6.0.0        |
| cms_setup               | 1.6.0.0.2      | 1.6.0.0.2      |
| compiler_setup          | 1.6.0.0        | 1.6.0.0        |
| complexworld_setup      | 0.1.0          | 0.1.0          |
| contacts_setup          | 1.6.0.0        | 1.6.0.0        |
| core_setup              | 1.6.0.6        | 1.6.0.6        |
| cron_setup              | 1.6.0.0        | 1.6.0.0        |
| customer_setup          | 1.6.2.0.4      | 1.6.2.0.4      |
| dataflow_setup          | 1.6.0.0        | 1.6.0.0        |
| directory_setup         | 1.6.0.3        | 1.6.0.3        |
| downloadable_setup      | 1.6.0.0.2      | 1.6.0.0.2      |
| eav_setup               | 1.6.0.1        | 1.6.0.1        |
| giftmessage_setup       | 1.6.0.0        | 1.6.0.0        |
| googleanalytics_setup   | 1.6.0.0        | 1.6.0.0        |
| importexport_setup      | 1.6.0.2        | 1.6.0.2        |
| index_setup             | 1.6.0.0        | 1.6.0.0        |
| log_setup               | 1.6.1.1        | 1.6.1.1        |
| moneybookers_setup      | 1.6.0.0        | 1.6.0.0        |
| newsletter_setup        | 1.6.0.2        | 1.6.0.2        |
| oauth_setup             | 1.0.0.0        | 1.0.0.0        |
| paygate_setup           | 1.6.0.0        | 1.6.0.0        |
| payment_setup           | 1.6.0.0        | 1.6.0.0        |
| paypaluk_setup          | 1.6.0.0        | 1.6.0.0        |
| paypal_setup            | 1.6.0.6        | 1.6.0.6        |
| persistent_setup        | 1.0.0.0        | 1.0.0.0        |
| poll_setup              | 1.6.0.1        | 1.6.0.1        |
| productalert_setup      | 1.6.0.0        | 1.6.0.0        |
| rating_setup            | 1.6.0.1        | 1.6.0.1        |
| reports_setup           | 1.6.0.0.1      | 1.6.0.0.1      |
| review_setup            | 1.6.0.0        | 1.6.0.0        |
| salesrule_setup         | 1.6.0.3        | 1.6.0.3        |
| sales_setup             | 1.6.0.9        | 1.6.0.9        |
| sendfriend_setup        | 1.6.0.1        | 1.6.0.1        |
| shipping_setup          | 1.6.0.0        | 1.6.0.0        |
| sitemap_setup           | 1.6.0.0        | 1.6.0.0        |
| tag_setup               | 1.6.0.0        | 1.6.0.0        |
| tax_setup               | 1.6.0.4        | 1.6.0.4        |
| usa_setup               | 1.6.0.3        | 1.6.0.3        |
| weblog_setup            | 0.1.0          | 0.1.0          |
| weee_setup              | 1.6.0.0        | 1.6.0.0        |
| widget_setup            | 1.6.0.0        | 1.6.0.0        |
| wishlist_setup          | 1.6.0.0        | 1.6.0.0        |
+-------------------------+----------------+----------------+
54 rows in set (0.00 sec)

这张表中包含所有已安装的模块及其版本号,在靠近下面的部分可以看到我们的模块

| weblog_setup            | 0.1.0          | 0.1.0          |

正是因为有这条记录,我们在第二次重新访问页面后安装脚本才没有运行,如果想要重新执行安装或升级脚本,比如在开发时想要查看效果,可以删除掉这条记录。那么现在我们就先删除掉这条记录并添加一条SQL语句来创建数据表,删除记录的代码为:

DELETE FROM core_resource WHERE code = 'weblog_setup';

另外我们还要先删除在ORM一节中所创建的blog_post一表,执行如下语句

DROP TABLE blog_posts;

然后在安装脚本(mysql4-install-0.1.0.php)中添加如下代码:

$installer =$this;
$installer-&gt;startSetup();
$installer-&gt;run(&quot;
	CREATE TABLE <code>{$installer-&gt;getTable('weblog/blogpost')}</code>(
		<code>blogpost_id</code> int(11) NOT NULL auto_increment,
		<code>title</code> text,
		<code>post</code> text,
		<code>date</code> datetime default NULL,
		<code>timestamp</code> timestamp NOT NULL default CURRENT_TIMESTAMP,
		PRIMARY KEY(<code>blogpost_id</code>)
		)ENGINE=InnoDB DEFAULT CHARSET=utf8;
	INSERT INTO <code>{$installer-&gt;getTable('weblog/blogpost')}</code> VALUES
	(1,'My New Title','This is a blog post','2015-8-09 00:00:00','2015-8-10 23:12:30'
		)
&quot;);
$installer-&gt;endSetup();

删除缓存重新加载任意页就会创建blog_posts数据表并插入一行内容。

接下来我们就一起来分析一下上面代码的含义

$installer =$this;

这句代码中的this指的是什么呢?每个安装脚本都在Setup资源类的环境中运行,自然$this就指向这个类的实例化对象。习惯上我们都会像本例中那样在安装脚本中为$this取一个别名$installer,没有特殊原因建议你在开发过程中也照做。

$installer->startSetup();
//...
$installer->endSetup();

紧接着的就是上面的代码段,如果查看app/code/core/Mage/Core/Model/Resource/Setup.php中的Mage_Core_Model_Setup类(也就是我们这个安装脚本所继承的类),会发现这些方法会进行了一些基本的SQL操作

public function startSetup()
    {
        $this->getConnection()->startSetup();
        return $this;
    }

    /**
     * Prepare database after install/upgrade
     *
     * @return Mage_Core_Model_Resource_Setup
     */
    public function endSetup()
    {
        $this->getConnection()->endSetup();
        return $this;
    }

查看Varien_Db_Adapter_Pdo_Mysql(lib/Varien/Db/Adapter/Pdo/Mysql.php)就可以找到上面用到的startSetup()和endSetup()方法

 public function startSetup()
    {
        $this->raw_query("SET SQL_MODE=''");
        $this->raw_query("SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0");
        $this->raw_query("SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'");

        return $this;
    }

    /**
     * Run additional environment after setup
     *
     * @return Varien_Db_Adapter_Pdo_Mysql
     */
    public function endSetup()
    {
        $this->raw_query("SET SQL_MODE=IFNULL(@OLD_SQL_MODE,'')");
        $this->raw_query("SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS=0, 0, 1)");

        return $this;
    }

另外安装脚本中清空调用了run方法

$installer->run(...);

该方法中传入了一个用于创建数据表的SQL语句,可传入多条SQL语句,只需使用分号进行分隔,可能大家还注意到了下面这条语句:

$installer->getTable('weblog/blogpost')

这个getTable方法中传入的是模型URI,并获取表名。虽然这里可以直接写入表名,但为避免因他人修改配置文件中的表名而导致脚本无法运行,建议养成使用这个方法的好习惯。Mage_Core_Model_Resource_Setup类中包含许多像这样有用的helper方法,最好的学习方法是去研究Magento中core模块的安装脚本。

从1.6开始,Magento就开始支持MySQL以外的数据库,既然我们的安装脚本中包含原生SQL语句,那有可能在其它的数据库系统比如MSSQL中无法执行。这也是为什么我们在文件名中添加了mysql4-作为前缀。

为确保安装脚本保持在多个数据库系统中兼容,Magento提供了一个DDL(Data Definition Language)数据表对象,以下我们对脚本进行修改来保证脚本能在所有Magento支持的关系型数据库中运行。

依然编辑app/code/local/Alanhou/Weblog/sql/weblog_setup/mysql4-install-0.1.0.php文件:

$installer = $this;
$installer->startSetup();
$table = $installer->getConnection()->newTable($installer->getTable('weblog/blogpost'))
->addColumn('blogpost_id',Varien_Db_Ddl_Table::TYPE_INTEGER,null,array(
	'unsigned'=>true,
	'nullable'=>false,
	'primary'=>true,
	'identity'=>true,
	),'Blogpost ID')
->addColumn('title',Varien_Db_Ddl_Table::TYPE_TEXT,null,array(
	'nullable'=>false,
	),'Blogpost Title')
->addColumn('post',Varien_Db_Ddl_Table::TYPE_TEXT,null,array(
	'nullable'=>true,
	),'Blogpost Body')
->addColumn('date',Varien_Db_Ddl_Table::TYPE_DATETIME,null,array(
	),'Blogpost Date')
->addColumn('timestamp',Varien_Db_Ddl_Table::TYPE_TIMESTAMP,null,array(
	),'Timestamp')
->setComment('Alanhou weblog/blogpost entity table');
$installer->getConnection()->createTable($table);

$installer->endSetup();

可以看到这里我们没有使用原生SQL语句,使用以上DDL形式的脚本可以保证咱们的模块在与各种数据库保持兼容。

现在我们已经掌握了如何通过脚本来创建数据表,可是如果我们想要对已有模块的数据表结构进行修改又该怎么做呢?Magento的安装资源允许我们通过版本号来自动运行脚本升级模块。一旦在Magento中运行了安装脚本,除非删除core_resource表中对应的记录,否则这个脚本就再也不会被运行。通过创建类似于安装脚本的升级脚本,我们就可以进行适当的升级操作。

创建app/code/local/Alanhou/Weblog/sql/weblog_setup/upgrade-0.1.0-0.2.0.php文件并加入以下代码:

<?php
echo "Testing our upgrade script(upgrade-0.1.0-0.2.0.php) and halting execution to avoid updating the system version number
";
die();

升级脚本和安装脚本放在同一个文件夹内,文件名前面为upgrade,后面为升级前的版本号和升级后的版本后,中间用”-“来进行分隔。如果我们清除缓存重新访问页面这个脚本并不会运行,需要修改config.xml中的版本号都会运行升级脚本:

<modules>
	<Alanhou_Weblog>
		<version>0.2.0</version>
	</Alanhou_Weblog>
</modules>

在这里我们的升级脚本也可以命名为mysql4-upgrade-0.1.0-0.2.0.php,当然这就表明了脚本针对的是MySQL。

在完善升级脚本前,还要一件需要注意的事情,先在同一个文件夹中创建app/code/local/Alanhou/Weblog/sql/weblog_setup/upgrade-0.1.0-01.5.php并加入代码:

<?php
echo "Testing our upgrade script(upgrade-0.1.0-0.1.5.php) and NOT halting execution
";

 

重新访问页面会同时出现两条提示

Testing our upgrade script(upgrade-0.1.0-0.1.5.php) and NOT halting execution
Testing our upgrade script(upgrade-0.1.0-0.2.0.php) and halting execution to avoid updating the system version number

这是因为当Magento发现模块的版本号发生变时,它会运行所有比当前版本号高的脚本,虽然我们并没有在模块配置文件中添加0.1.5,但该脚本依然会被执行。脚本的执行顺序是按版本号从低到高,此时再查看core_resource会得到如下结果:

mysql> select * from core_resource where code = 'weblog_setup';
+--------------+---------+--------------+
| code         | version | data_version |
+--------------+---------+--------------+
| weblog_setup | 0.1.5   | 0.1.5        |
+--------------+---------+--------------+
1 row in set (0.00 sec)

可以看到版本号已经变成0.1.5了,这说明0.1.0到0.1.5的升级已经完成,并不是执行的0.1.0到0.2.0的升级。那么下面我们就来修改0.1.0-0.2.0的升级脚本:

<?php
 $installer = $this;
$installer->startSetup();
$installer->getConnection()
->changeColumn($installer->getTable('weblog/blogpost'),'post',
	'post',array(
		'type'=>Varien_Db_Ddl_Table::TYPE_TEXT,
		'nullable'=>false,
		'comment'=>'Blogpost Body'
		)
	);
$installer->endSetup();
die("You'll see why this is here in a second");

 

重新刷新页面发现升级脚本并没有执行,post列依然允许使用空值,更重要的是die()语句并没有使用程序跳出运行,到底发生了什么呢?我们来一起捋一下前面的操作:

  1. 起初weblog_setup资源的版本号是0.1.0
  2. 我们将模块升级到0.2.0
  3. Magento发现了升级模块,并且有两个升级脚本(0.1.0到0.1.5和0.1.0到0.2.0)
  4. Magento将两个脚本放到队列中运行
  5. Magento运行了0.1.0到0.1.5的脚本
  6. 现在weblog_setup资源的版本号是0.1.5
  7. 运行0.1.0到0.2.0升级脚本,执行被中断
  8. 在进行下一页面加载时,Magento发现weblog_setup的版本号是0.1.5且未发现可升级的脚本,因为两个脚本都是0.1.0的升级脚本

正确的做法是把脚本名称改做upgrade-0.1.0-0.1.5.php和upgrade-0.1.5-1.2.0.php

这样Magento就会一次加载中同时运行这两个脚本,可以通过更新core_resource表来重新进行升级:

UPDATE core_resource SET version = '0.1.0', data_version = '0.1.0' WHERE code = 'weblog_setup';

删除掉upgrade-0.1.5-0.2.0.php再重新加载页面,两个升级脚本就会同时运行,这时再去查看blog_posts表中的post列已经不允许为空了。

所以在多人开发同一个Magento项目时就需要格外注意升级版本的问题,避免会出现错误。

 

退出移动版