Django的MySQL Driver配置

之前写过一篇文章《Django@Python3添加MySQL/MariaDB支持》,现在Django对MySQL的支持已经很好了,现在就说一说最佳实践吧。

PEP 249 - Python Database API Specification v2.0规定了Python的数据库API。主要有三种API实现:

  • MySQLdb 是Andy Dustman开发的、使用原生代码/C语言绑定的驱动,它已经开发了数十年。
  • mysqlclient 是MySQLdb的一个支持Python3的fork,并且可以无缝替换调MySQLdb。mysqlclient目前是MySQL在Django下的推荐选择。
  • MySQL Connector/Python 是Oracle写的,纯Python实现的客户端库。

以上所有的驱动都是线程安全的,且提供了连接池。MySQLdb 是唯一一个不支持Python3的。

如果你使用mysqlclient(推荐此方式)

settings.py中的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', #数据库引擎
'NAME': 'test', #数据库名
'USER': 'root', #用户名
'PASSWORD': 'root', #密码
'HOST': '', #数据库主机,默认为localhost
'PORT': '', #数据库端口,MySQL默认为3306
'OPTIONS': {
'autocommit': True,
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
},
}
}

第14行主要是为了防止警告:

(mysql.W002) MySQL Strict Mode is not set for database connection 'default'

当然,要在requirements.txt中添加对mysqlclient的依赖:


php-fpm的reload过程

背景

谈谈PHP的Reload操作中提到reload会让sleep提前结束,所以就探究了下fpm的reload操作如何实现。

本文在php7.0 fpm下分析,process_control_timeout设置不为0。


重启信号

首先,我们从PHP源码可以知道,fpm的reload操作实际上就是对fpm进程发送了USR2信号。

程序在处理信号的时候,主进程的逻辑相当于“暂停”了,如果在这儿执行一些操作的话,第一,有些局部变量拿不到;第二,可能会打断主进程的逻辑。所以,信号处理函数仅仅是通知主进程,用户发送了这个信号。

信号处理函数的注册

fpm的master进程中,fpm_signals_init_main函数通过sigaction注册了信号处理函数sig_handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int fpm_signals_init_main() /* {{{ */
{
struct sigaction act;

// 。。。。。。

memset(&act, 0, sizeof(act));
act.sa_handler = sig_handler;
sigfillset(&act.sa_mask);

if (0 > sigaction(SIGTERM, &act, 0) ||
0 > sigaction(SIGINT, &act, 0) ||
0 > sigaction(SIGUSR1, &act, 0) ||
0 > sigaction(SIGUSR2, &act, 0) ||
0 > sigaction(SIGCHLD, &act, 0) ||
0 > sigaction(SIGQUIT, &act, 0)) {

zlog(ZLOG_SYSERROR, "failed to init signals: sigaction()");
return -1;
}
return 0;
}
/* }}} */

composer的自动加载机制解读

按照composer文档的说法,如果是composer项目,只需要在开始的时候require 'vendor/autoload.php'即可享受类的自动加载特性。可是这是如何实现的呢?

vendor/autoload.php

以Laravel 5.1项目为例,vendor/autoload.php文件只做了两件事情:

  1. include vendor/composer/autoload_real.php
  2. 调用ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b::getLoader()

vendor/composer/autoload_real.php仅仅定义了ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b类和composerRequireb6d254015e39cf5090fb84fdb1ed664b函数。(b6d254015e39cf5090fb84fdb1ed664b应该是类似id一样的东西,确保每次不同) 接下来我们关注下ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b::getLoader()做了哪些事情。

ComposerAutoloaderInit<id>::getLoader()

首先,这个类的loader只会初始化一次,第二次是直接返回已经存在的loader了:

1
2
3
if (null !== self::$loader) {
return self::$loader;
}

如果是第一次调用,先注册['ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b', 'loadClassLoader'],然后new一个\Composer\Autoload\ClassLoader 作为loader,然后立马取消注册loadClassLoader。 接下来就一步一步处理各种autoload了。

autoload_namespaces.php


Laravel Facade实现细节考

前两天有人讲Laravel中的Facade的时候,看到了__callStatic的实现,探究了下为何如此。

现有实现

switch实现

我们在调用Facede的方法的时候,绝大多数都会被__callStatic来处理,Larvel 5.1的__callStatic实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch (count($args)) {
case 0:
return $instance->$method();
case 1:
return $instance->$method($args\[0\]);
case 2:
return $instance->$method($args\[0\], $args\[1\]);
case 3:
return $instance->$method($args\[0\], $args\[1\], $args\[2\]);
case 4:
return $instance->$method($args\[0\], $args\[1\], $args\[2\], $args\[3\]);
default:
return call\_user\_func\_array(\[$instance, $method\], $args);
}

这个实现是 Taylor Otwell 最初实现的版本

cufa实现

是上述switch实现的简化版:

1
return call_user_func_array([$instance, $method], $args);

Argument Unpack实现


composer中指定依赖分支名的坑

之前只是看着别人写的composer.json,知道如果依赖一个项目的master分之,则在依赖的version中可以写dev-master。我就以为所有对分支的依赖,都是写成dev-<branch_name>

后来发现,v1.x这种分支名,不能直接使用dev-v1.x来声明依赖。 查了下官方文档

For every branch, a package development version will be created. If the branch name looks like a version, the version will be {branchname}-dev. For example, the branch 2.0 will get the 2.0.x-dev version (the .x is added for technical reasons, to make sure it is recognized as a branch). The 2.0.x branch would also be valid and be turned into 2.0.x-dev as well. If the branch does not look like a version, it will be dev-{branchname}. master results in a dev-master version.

翻译如下:

对于每个分支,对应的包都会创建一个开发版本。如果分支名看起来像一个版本号,则开发版本的名字是{branchname}-dev。 比如,分支2.0则会创建2.0.x-dev版本(后面的 .x 是为了技术原因添加,为了确保这个版本能够被识别为分支)。 2.0.x 分支也会被转换为 2.0.x-dev。如果分支看起来不像一个版本,版本号将会是dev-{branchname}。master分支的版本号是dev-master。


另外,当同时存在1.0 tag 和1.0.x分支时,1.0.x-dev指向的是1.0分支


Laravel Migration 类名重复分析

开发者在开发中一般都会为Migration起类名,最常见的就是AlterUserTable这种名字,但是如果后续的开发者第二次修改表,就有了两个类名相同的Migration了。 这样有什么问题吗? 首先,这个Migration如果和之前类名相同的Migration不在同一批次的话,是可以执行成功的。 接下来看看为什么。

Migration的执行

从Migration的执行来看,\Illuminate\Database\Migrations\Migrator::getMigrationFiles拿到所有的Migration列表,通过和执行过的Migration array_diff之后,获取没有执行的Migration列表,在执行这些Migration的时候会在\Illuminate\Database\Migrations\Migrator::requireFiles方法中require对应的Migration文件,如果恰好两个类名相同的Migration需要执行,那么就会出现错误 Cannot declare class AlterTestTable, because the name is already in use in ./database/migrations/2016_07_20_081952_alter_test_table.php on line 31 当然,如果这些Migration都在不同批次中,那么永远也不会有这个错误发生了嘛。

Migration的回退

\Illuminate\Database\Migrations\MigrationRepositoryInterface::getLast获取最后一次执行的Migration文件名列表,然后在\Illuminate\Database\Migrations\Migrator::runDown方法中将Migration名字resolve到类名(比如 2016_07_20_081952_alter_test_tableAlterTestTable 类,这时候就通过new这个类,运行down方法来回退。 同样的,在这种情况下,如果有两个一样名字的类,则autoload机制只会选择一个。 注意,在这种情况下,即使两个Migration不在同一个批次当中回退,那也会有一个Migration永远不能回退。


  • 这也解释了为什么在本地创建了migration后,运行后,然后rollback的时候提示找不到这个类,而必须要dumpautoload才可以。
  • 由于Migration up的时候是通过Laravel自己解析到代码的,而down的时候是通过composer解析的,这导致了Migration的up和down不对称,确实不怎么好看。
  • 作为一个合格的开发,显然不应该让Migration类名重复!

PHP 7.0中,目前不能定义函数的返回类型为null或者void

TL;DR:

  • 在PHP 7.0下,不要将函数的返回值声明为null或者void,在PHP7.1下可以。
  • 目前函数返回值不可被声明为nullable。

引子

今天有开发同学遇到了定义返回值为null的函数无法被load的问题: Cannot use ‘App\null’ as class name as it is reserved 追查发现,是因为函数的返回值被声明为了null。

追查

首先,看看返回值到底能不能声明为null: 在RFC PHP RFC: Return Type Declarations 中的Future Work中说:

Allow functions to declare that they do not return anything at all (void in Java and C)

这说明此RFC并不支持声明返回值为null或者void! 但是 PHP RFC: Void Return Type 则表示支持void表示没有任何返回值,但是不允许用null来声明!(还给了原因) 但此RFC会在PHP 7.1中实现,所以目前在PHP 7.0中还不能使用。 PS:经过实验,PHP7.0中,声明返回值为void可以通过语法检查,但是无论怎么返回都会报错(无论是直接return还是不返回):

Return value of App\User::func() must be an instance of void, none returned

更远一点,关于声明返回值为nullable的问题,现在还在草稿阶段,还有很长的路要走:PHP RFC: Declaring Nullable Types


安装 PHP 7

PHP 7 正式发布了好久了,现在就总结下如何在各个系统上安装 PHP 7。(本文在很大程度上参考了 Installing PHP 7.0.0

Ubuntu系列

PHP 7 可以直接使用 PPA for PHP (5.6, 7.0) : Ondřej Surý:(原来这个PPA中的fpm不能正常工作,后来发现已经改掉了)

sudo add-apt-repository ppa:ondrej/php
sudo apt-get update
sudo apt-get install php7.0

Debian系列

PHP 7 可以通过 Dotdeb repository 来安装: 创建文件 /etc/apt/sources.list.d/dotdev.list ,内容为如下两行:(其中的 <distribution> 根据需要替换为 squeeze, wheezy 或者 jessie):

deb http://packages.dotdeb.org all
deb-src http://packages.dotdeb.org all

添加GPG key :

wget https://www.dotdeb.org/dotdeb.gpg
sudo apt-key add dotdeb.gpg

安装 PHP 7 :


PHP7 中五个鲜有人知的特性

本文是 Five Lesser-Known Features of PHP 7 的译文 PHP 7 即将发布(原文如此,现在 PHP 7 已经正式发布),我觉得检查 PHP 7 带来的一些鲜为人知的新特性是一件非常酷的事情:

1. define()可以定义数组常量

自 PHP 5.6 开始,可以使用 const 关键字在类中定义常量数组:

const LUCKY_NUMBERS = [4, 8, 15, 16, 23, 42];

PHP 7 将同样的功能引入到 define() 函数中:

define(‘LUCKY_NUMBERS’, [4, 8, 15, 16, 23, 42]);

2. 被0除

PHP 7 之前,被0除会导致一条 E_WARNING 并返回 false。一个数字运算返回一个布尔值是没有意义的,所以 PHP 7 会返回如下的 float 值之一(同时出发一条 E_WARNING):

  • +INF
  • -INF
  • NAN

比如:


PHP/Laravel中的“异常链”

之前知道,Python 3 中终于实现了对异常链的支持。 最近,在代码中,有一处写着:

try {
// some code
} catch (\Exception $e) {
\Log::error(‘xxx_id’ . $xxx->id);
}

问了之后才发现,这是需要知道是哪个id触发的错误,如果不捕获异常的话,在backtrace信息中可能不会出现此id。 但是,这样做虽然可以达到他的目的,但是掩盖了底层的错误,对后续的追查不利。 然后,作为(伪)Python开发者的我就想起了Python3中的异常链技术,所以找了下PHP中有没有类似的东西,结果发现:自PHP 5.3开始,支持了类似的技术。 解决方案:可以自己扩展Excepiton类,默认的构造方法里面,第三个参数是前一个异常,经过测试,在Laravel框架中,同一条链中的Exception都会展示出来(无论是在Console输出,还是日志输出中)。


好,让我们来自己试一试。 首先写一个Command:

class NestExceptionTest extends Command
{
protected $name = ‘NestExceptionTest’;

public function fire()
{
    try {
        $this->testOneLevel();
    } catch (\\Exception $e) {
        throw new \\Exception("top level", 0, $e);
    }
}

private function testOneLevel()
{
    try {
        $this->testTwoLeve();
    } catch (\\Exception $e) {
        throw new \\Exception('one level', 0, $e);
    }
}

private function testTwoLeve()
{
    throw new \\Exception('two level');
}

}

为了偷懒,就没有自己定义自己的Exception,仅仅是不同的错误信息来区分。 执行此命令,输出如下: Exception output console同时,日志输出如下:

[2015-12-12 13:54:06] production.ERROR: exception ‘Exception’ with message ‘two level’ in /Users/robberphex/projects/xxx/app/Console/Commands/NestExceptionTest.job:31
Stack trace:
#0 /Users/robberphex/projects/xxx/app/Console/Commands/NestExceptionTest.job(23): App\Console\Commands\SiteMap\SiteMapBuild->testTwoLeve()
#1 /Users/robberphex/projects/xxx/app/Console/Commands/NestExceptionTest.job(14): App\Console\Commands\SiteMap\SiteMapBuild->testOneLevel()
#2 [internal function]: App\Console\Commands\SiteMap\SiteMapBuild->fire()
#3 /Users/robberphex/projects/xxx/vendor/laravel/framework/src/Illuminate/Container/Container.php(503): call_user_func_array(Array, Array)
#4 /Users/robberphex/projects/xxx/vendor/laravel/framework/src/Illuminate/Console/Command.php(150): Illuminate\Container\Container->call(Array)
#5 /Users/robberphex/projects/xxx/vendor/symfony/console/Command/Command.php(256): Illuminate\Console\Command->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#6 /Users/robberphex/projects/xxx/vendor/laravel/framework/src/Illuminate/Console/Command.php(136): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#7 /Users/robberphex/projects/xxx/vendor/symfony/console/Application.php(838): Illuminate\Console\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#8 /Users/robberphex/projects/xxx/vendor/symfony/console/Application.php(189): Symfony\Component\Console\Application->doRunCommand(Object(App\Console\Commands\SiteMap\SiteMapBuild), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#9 /Users/robberphex/projects/xxx/vendor/symfony/console/Application.php(120): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#10 /Users/robberphex/projects/xxx/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(107): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#11 /Users/robberphex/projects/xxx/artisan(36): Illuminate\Foundation\Console\Kernel->handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#12 {main}


Robert Lu

关注我的公众号