如何写一个属于自己的数据库封装(2) - 数据库连接(修订版)

上一期 如何写一个属于自己的数据库封装(1) - 基本思路
下一期 如何写一个属于自己的数据库封装(3) - 查询 - 入门篇

本期要点

还是PDO 中文文档


Connector.php

  • 负责与数据库通信,增删改读(CRUD)

首先, 建一个Connector类, 并且设置属性

<?php
class Connector {
    // 数据库地址前缀,常见的有mysql,slqlsrv,odbc等等等
    private $driver = 'mysql';
    // 数据库地址
    private $host = 'localhost';
    // 数据库默认名称, 设置为静态是因为有切换数据库的需求
    private static $db = 'sakila';
    // 数据库用户名
    private $username = 'root';
    // 数据库密码
    private $password = '';
    // 当前数据库连接
    protected $connection;
    // 数据库连接箱,切换已存在的数据库连接不需要重新通信,从这里取即可
    protected static $container = [];

    // PDO默认属性配置,具体请自行查看文档
    protected $options = [
        PDO::ATTR_CASE => PDO::CASE_NATURAL,
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
        PDO::ATTR_STRINGIFY_FETCHES => false,
    ];
}

以上代码配合注释应该可以理解了所以不多解释了,直接进入函数

  • buildConnectString - 生成DSN连接串
    protected function buildConnectString() {
        return "$this->driver:host=$this->host;dbname=".self::$db;
    }

以上函数依照最开始设置的类属性, 将返回字符串

"mysql:host=localhost;dbname=sakila;"
  • connect - 连接数据库
    public function connect() {
        try {
            // 连接数据库,生成pdo实例, 将之赋予$connection,并存入$container之中
            self::$container[self::$db] = $this->connection = new PDO($this->buildConnectString(), $this->username, $this->password, $this->options);
            // 返回数据库连接
            return $this->connection;
        } catch (Exception $e) {
            // 若是失败, 返回原因
            // 还记得dd()吗?这个辅助函数还会一直用上
            dd($e->getMessage());
        }
    }
  • setDatabase - 切换数据库
    public function setDatabase($db) {
        self::$db = $db;
        return $this;
    }
  • _construct - 生成实例后第一步要干嘛
    function __construct() {
        // 如果从未连接过该数据库, 那就新建连接
        if(empty(self::$container[self::$db]))
            $this->connect();
        // 反之, 从$container中提取, 无需再次通信
        $this->connection = self::$container[self::$db];
    }

接下来两个函数式配合着用的,单看可能会懵逼, 配合以下例子单步调试

$a = new Connector();

$bindValues = [
    'PENELOPE',
    'GUINESS'
];

dd($a->read('select * from actor where first_name = ? and last_name = ?', $bindValues));

返回结果

array (size=1)
  0 =>
    object(stdClass)[4]
      public 'actor_id' => string '1' (length=1)
      public 'first_name' => string 'PENELOPE' (length=8)
      public 'last_name' => string 'GUINESS' (length=7)
      public 'last_update' => string '2006-02-15 04:34:33' (length=19)
  • read - 读取数据
    1. 将例子中的 sql 语句 (select * from actor where first_name = ? and last_name = ?) 代入 $sql
    2. 两个问号分别对应了两个需要筛选的条件, 将之带入自定义函数bindValues之中循环调用PDO的bindValue函数
    3. 最后执行并返回Object类型的数据
    public function read($sql, $bindings) {
        // 将sql语句放入预处理函数
        // $sql = select * from actor where first_name = ? and last_name = ?
        $statement = $this->connection->prepare($sql);
        // 将附带参数带入pdo实例
        // $bindings = ['PENELOPE', 'GUINESS']
        $this->bindValues($statement, $bindings);
        // 执行
        $statement->execute();
        // 返回所有合法数据, 以Object对象为数据类型
        return $statement->fetchAll(PDO::FETCH_OBJ);
    }
  • bindValues - 将附带参数带入pdo实例

    1. 在read函数的第2步中将两个条件 ['PENELOPE', 'GUINESS'] 带入
    2. 循环调用PDO的bindValue函数

    从例子中可以看出, 我用在预处理的变量为?, 而不是变量 (:first_name), 这是因为PDO的局限性, bindValue函数并不支持首字符数字或符号, 如果使用变量绑定, 需要写一整段的判定来去除数字和符号, 因此效率至上, 选择?绑定

    public function bindValues($statement, $bindings) {
        // $bindings = ['PENELOPE', 'GUINESS']
        // 依次循环每一个参数
        foreach ($bindings as $key => $value) {
            // $key = 0/1
            // $value = 'PENELOPE'/'GUINESS'
            $statement->bindValue(
                // 如果是字符串类型, 那就直接使用, 反之是数字, 将其+1
                // 这里是数值, 因此返回1/2
                is_string($key) ? $key : $key + 1,
                // 直接放入值
                // 'PENELOPE'/'GUINESS'
                $value,
                // 这里直白不多说
                // PDO::PARAM_STR/PDO::PARAM_STR
                is_int($value) || is_float($value) ? PDO::PARAM_INT : PDO::PARAM_STR
            );
        }
    }
  • update - 改写数据
    // 与read不同的地方在于, read返回数据, update返回boolean(true/false)
    public function update($sql, $bindings) {
        $statement = $this->connection->prepare($sql);
        $this->bindValues($statement, $bindings);
        return $statement->execute();
    }
  • delete - 删除数据
    // 逻辑与update完全一致, 分开是因为方便日后维护制定
    public function delete($sql, $bindings) {
        $statement = $this->connection->prepare($sql);
        $this->bindValues($statement, $bindings);
        return $statement->execute();
    }
  • create - 插入数据
    // 返回最新的自增ID, 如果有, 否则返回null
    public function create($sql, $bindings) {
        $statement = $this->connection->prepare($sql);
        $this->bindValues($statement, $bindings);
        $statement->execute();
        return $this->lastInsertId();
    }
  • lastInsertId - 返回新增id, 如果有, 否则返回null

    仅仅封装了PDO自带的函数, 因此存在PDO自身的问题(或是说缺陷?)

    在处理业务逻辑的时候, 批量插入数据可能需要返回最后一条插入的数据, 实际上PDO自带的 lastInsertId 函数返回的是第一条数据的ID

    比方说一个已存在三条数据的表, ID => 1, 2, 3

    插入3条新数据, 自增ID => 4, 5, 6

    lastInsertId 函数 返回 4

    public function lastInsertId() {
        $id = $this->connection->lastInsertId();
        return empty($id) ? null : $id;
    }

过于高级复杂的SQL语句可能无法封装, 因此准备了可直接用RAW query通信数据库的两个函数

  • exec - 适用于增删改(建议)
    public function exec($sql) {
        return $this->connection->exec($sql);
    }
  • query - 适用于读(建议)
    public function query($sql) {
        $q = $this->connection->query($sql);
        return $q->fetchAll(PDO::FETCH_OBJ);
    }

将数据库事务相关的函数封装起来, 直白所以没有注释

    public function beginTransaction() {
        $this->connection->beginTransaction();
        return $this;
    }

    public function rollBack() {
        $this->connection->rollBack();
        return $this;
    }

    public function commit() {
        $this->connection->commit();
        return $this;
    }

    public function inTransaction() {
        return $this->connection->inTransaction();
    }

完整代码

我放在了Coding.net

本期疑问

因为PHP本身的特性, 默认情况下运行完所有代码类会自行析构,PDO自动断开联系, 所以我没有disconnect(),让PDO断开连接, 不知这样是不是一种 bad practice?

欢迎点赞或下方评论, 看了看感觉自己写的有些不利落, 请指出

上一期 如何写一个属于自己的数据库封装(1) - 基本思路
下一期 如何写一个属于自己的数据库封装(3) - 查询 - 入门篇

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容