Medoo源码笔记

Medoo为何物

Medoo是一个使用PHP实现的轻量级数据库封装库,支持常见的数据库,包括:MySQL,MariaDB,PostgreSQL,Sybase,Oracle,SQLite,SQL Server。 此库基于PDO实现,基于PDO的抽象接口以及在此基础之上细微的调整,实现了对各大数据库的支持。

根据官网介绍,Medoo的存在是为了提高数据库应用的开发效率,主要有以下几个特点:

  • 轻量级

    单文件类,代码仅1000行左右。除了PDO相关扩展的依赖外(使用PDO方式无法避免的),没有其他额外的依赖关系,直接拖到项目下就可以使用。

  • 简单易用

    通过对SQL语法的重新设计(主要简化条件子句、链表子句),使得上层代码更加简洁清晰,从而加快开发速度。

  • 强大

    支持各种常见的增删改查操作,如果需要的SQL语句实在太过复杂,无法使用抽象的函数接口实现,可以通过调用底层接口实现。

  • 兼容性

    支持多种主流数据库服务器,本文开头已经提及。

  • 安全

    防止SQL注入,使得上层应用不用关注SQL注入,集中精力实现业务逻辑。

API一览

以下是结合代码与官方文档整理的一份Medoo开放接口:

//增删改查
public function select($table, $join, $columns = null, $where = null)
public function insert($table, $datas)
public function update($table, $data, $where = null)
public function delete($table, $where)

//文本替换,基于数据库的replace()函数实现
public function replace($table, $columns, $search = null, $replace = null, $where = null)

//工具函数
public function get($table, $join = null, $column = null, $where = null)
public function has($table, $join, $where = null)
public function count($table, $join = null, $column = null, $where = null)

//简单的数学函数
public function max($table, $join, $column = null, $where = null)
public function min($table, $join, $column = null, $where = null)
public function avg($table, $join, $column = null, $where = null)
public function sum($table, $join, $column = null, $where = null)

//通用函数,如果上面已知的各种接口搞不定,还可以用这两个,直接传入完整的SQL语句,此时需要自行保证安全
//query返回结果集,用于查询;
//exec返回生效的行数,用于更新等操作。
public function query($query)
public function exec($query)

//调试接口
public function debug()
public function error()
public function last_query()
public function log()
public function info()

Medoo如何实现

下面着重介绍下其对于SQL语句的SQL抽象以及安全防护部分。

SQL抽象设计

原生的SQL语句不直观,重点不突出,看起来很费劲。Medoo对where子句、join子句等进行了抽象设计,使得PHP业务代码更加简洁清晰。这是Medoo能够加快开发的根本所在。

此部分可以对照官方文档查看。

  1. where子句

    从宏观看,where条件就是各个子条件的与或者或组合起来的。因此Medoo把where抽象成key是AND/OR,value是各子条件数组的数组。

    比如,以下代码对应最终的SQL语句id = 1024 and name = 'world'

    [
        'AND' => [
            'id' => 1024,
            'name' => 'world',
        ],
    ]
    

    对于同一个数组中需要有多个AND或者OR的情况,为了解决键名冲突的问题,引入注释,#前要有空格,如'AND #blahblah'。

    BUG:因为AND/OR也是合法的数据库表列名,因此以AND,OR关键字命名的数据表列使用时会有问题。如存在以下的数据表:

    mysql> show create table t_keyword_column;
    +------------------+-----------------------------------------------------------------------------------------------------+
    | Table            | Create Table                                                                                        |
    +------------------+-----------------------------------------------------------------------------------------------------+
    | t_keyword_column | CREATE TABLE "t_keyword_column" (
      "and" int(11) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
    +------------------+-----------------------------------------------------------------------------------------------------+
    1 row in set (0.34 sec)
    

    用这个Medoo语法就会报错,导致无法获取数据:

    $md->get('t_keyword_column', 'and', ['and' => 1]);
    
    1. 条件关系

      最小的条件单位就是各种值的判断了,Medoo对其中的key可以增加了语法规则,具体如下:

      • 数值型

        如下代码表示id != 1024

        [
            'id[!]' => 1024,
        ]
        

        除了叹号,还可以有>,>=,<,<=,<>,><,其中<>表示区间(><表示不在区间),如下代码表示id between 1024 and 2048

        [
            'id[<>]' => [1024, 2048],
        ]
        
      • 字符串

        字符串的LIKE可以通过~,!~表示是,否匹配,如下代码表示name not like '%world%'

        [
            'name[!~] => 'world',   //如果值有包含%,_,则最终表达式不会加%,_
        ]
        
      • 函数比较

        有时列需要与函数进行比较,比如常见的时间比较,可以通过在key前面加井号来表达(函数名需要大写,没想到有啥必要作这个限制)。如下代码表示xdate = CURDATE()

        [
            '#xdate' => 'CURDATE()',
        ]
        

        缺陷:此功能比较有限,无法支持带参的函数。

    2. 特殊的key

      有些用于限制结果的SQL条件,Medoo也使用了与条件关系相同的方式进行抽象,包括group...having,order by,limit。这里直接把官网文档贴过来:

      $database->select("account", "user_id", [
          "GROUP" => "type",
          // Must have to use it with GROUP together
          "HAVING" => [
              "user_id[>]" => 500
          ],
          // LIMIT => 20
          "LIMIT" => [20, 100]
      ]);
      //  SELECT user_id FROM account
      //  GROUP BY type
      //  HAVING user_id > 500
      //  LIMIT 20,100
      
  2. join子句

    联表操作也同样是借助数组实现的,key为要联结的表名、联结方式以及是否指定别名,value为具体联结的条件关系(数组表示有多个条件)。

    以下代码表示左联结:

    [
        "[>]account" => ["author_id" => "user_id"],     //post left join account on post.author_id = account.user_id
    ]
    

    除了左联结,<,<>,><分别表示右联结,全联结和内联结。

    缺陷:这几个符号比较抽象,不容易记住。其实表名反正不允许方括号,不如直接用[left]这样的形式更容易记忆呢。

    不同表相同字段名联结:

    [
        "[>]account" => ["author_id"],     //post left join account using(author_id)
    ]
    

    不同表的多个相同字段名联结:

    [
        "[>]account" => ["author_id", "user_id"],     //post left join account using(author_id, user_id)
    ]
    

安全防护设计

  1. SQL模式

    MySQL数据库类型时,会通过以下命令改变SQL模式为ANSI_QUOTESSET SQL_MODE=ANSI_QUOTES),这应该也是出于安全考虑,防止对双引号字符串滥用或者逻辑短路导致发生注入。

    此模式下双引号的作用与后引号(backtick)的作用相同,不能再用于字符串的表达。

  2. 各种转义

    Medoo的SQL注入防护方法通过转义实现(借助PDO的转义功能),对字符串(quote接口)、列名(column_quote接口)、函数名(fn_quote接口)等作了转义。

    比如,一个常见的SQL注入例子,得到的SQL语句是经过转义的,无法进行逻辑短路。

    $md->get('user', 'User', [
        "User" => "'' or '1' = '1'",
    ]);
    
    mysql> SELECT "User" FROM "user" WHERE "User" = '\'\' or \'1\' = \'1\'' LIMIT 1;
    Empty set (0.00 sec)
    

其他

PDO能够直接访问,暴露内部实现。

social