数据库增删改查操作

前言

laravel对操作数据库提供了原生sql、查询构建器和Eloquent ORM三种方式。今天我们来看看这三种方式的运行原理。

原生sql

原生sql和查询构建器都依赖静态依赖代理DB(也就是DatabaseManager)关于这个可以参考Facade,现在我们就以DB::select('select * from user where name = ?', ['test']),这个为例看看DB是如何运行的。在调用select方法时,DatabaseManager中没有就会调用魔术方法__call(),在__call中会先调用connection方法生成一个数据库连接对象然后调用数据库连接对象里的select。

生成数据库连接对象

生成数据库连接对象,先获取数据库的连接名和连接类型(read、write、null),并且从app/config/database中获取要连接的数据库配置预先生成一个连接数据库连接器闭包,利用数据库连接工厂生成一个数据库连接对象。

class DatabaseManager
{
  public function __call($method, $parameters)
    {
        return $this->connection()->$method(...$parameters);
    }
    
  /**
* 返回一个数据库连接对象
**/
    public function connection($name = null)
    {
        list($database, $type) = $this->parseConnectionName($name);
        //$name = 'mysql';
        $name = $name ?: $database;
       
       //这里会把已经生成的连接保存在数组中,供下一次使用
        if (! isset($this->connections[$name])) {
            $this->connections[$name] = $this->configure(
                $this->makeConnection($database), $type
            );
        }

        return $this->connections[$name];
    }
   /**
* 获取数据库连接名和类型
**/
    protected function parseConnectionName($name)
    {
        $name = $name ?: $this->getDefaultConnection();
        //['mysql',null]
        return Str::endsWith($name, ['::read', '::write'])
                            ? explode('::', $name, 2) : [$name, null];
    }

    public function getDefaultConnection()
    {
        //获取config目录下database数组中的default的值,默认为mysql
        return $this->app['config']['database.default'];
    }

  /**
* 利用数据库连接工厂创建数据库连接对象
**/
    protected function makeConnection($name)
    {
        //获取了mysql连接的基本配置数组
        $config = $this->configuration($name);

        //这里的factory就是ConnectionFactory具体参看DatabaseServiceProvider中的register  
        return $this->factory->make($config, $name);
    }
   /**
* 获取数据库的配置信息
***/
    protected function configuration($name)
    {
      //获取对应config目录中database数组中connections中键名为$name的数据库连接配置这里的$name默认为mysql
        $name = $name ?: $this->getDefaultConnection();

       $connections = $this->app['config']['database.connections'];

        if (is_null($config = Arr::get($connections, $name))) {
            throw new InvalidArgumentException("Database [{$name}] not configured.");
        }

        return $config;
    }

}

class ConnectionFactory
{
  public function make(array $config, $name = null)
    {
        //这里给config配置数组添加了name和prefix两个键
        $config = $this->parseConfig($config, $name);

        if (isset($config['read'])) {
            return $this->createReadWriteConnection($config);
        }

        return $this->createSingleConnection($config);
    }

protected function createSingleConnection(array $config)
    {
      //返回一个连接数据库的闭包
        $pdo = $this->createPdoResolver($config);

       创建一个数据库连接对象MySqlConnection
        return $this->createConnection(
            $config['driver'], $pdo, $config['database'], $config['prefix'], $config
        );
    }

  protected function createPdoResolver(array $config)
    {
      //这里对于有多个host配置的会随机获取其中一个host去连接
        return function () use ($config) {
            return $this->createConnector($config)->connect($config);
        };
    }

    protected function createConnection($driver, $connection, $database, $prefix = '', array $config = [])
    {
        switch ($driver) {
            case 'mysql':
                return new MySqlConnection($connection, $database, $prefix, $config);
            case 'pgsql':
                return new PostgresConnection($connection, $database, $prefix, $config);
            case 'sqlite':
                return new SQLiteConnection($connection, $database, $prefix, $config);
            case 'sqlsrv':
                return new SqlServerConnection($connection, $database, $prefix, $config);
        }

        throw new InvalidArgumentException("Unsupported driver [{$driver}]");
    }
  
    public function createConnector(array $config)
    {
        if (! isset($config['driver'])) {
            throw new InvalidArgumentException('A driver must be specified.');
        }

        if ($this->container->bound($key = "db.connector.{$config['driver']}")) {
            return $this->container->make($key);
        }

        switch ($config['driver']) {
            case 'mysql':
                return new MySqlConnector;
            case 'pgsql':
                return new PostgresConnector;
            case 'sqlite':
                return new SQLiteConnector;
            case 'sqlsrv':
                return new SqlServerConnector;
        }

        throw new InvalidArgumentException("Unsupported driver [{$config['driver']}]");
    }

  
}
class MysqlConnector
{
  public function connect(array $config)
    {

        //生成连接PDO时需要的dsn字符串 比如:'mysql:host=127.0.0.1;port=3306;dbname=test'
        $dsn = $this->getDsn($config);
      
       //生成连接PDO时的可选参数
        $options = $this->getOptions($config);

       //得到一个PDO连接对象
        $connection = $this->createConnection($dsn, $config, $options);
      
       // 选择数据库
        if (! empty($config['database'])) {
            $connection->exec("use `{$config['database']}`;");
        }
        
      //设置字符集和排序
        $this->configureEncoding($connection, $config);
       //设置时区
        $this->configureTimezone($connection, $config);
       
       //设置数据库的model这个有机会的话想说说这个设置,平时不注意可能会在这里踩坑
        $this->setModes($connection, $config);

        return $connection;
    }
   /**
  * 生成PDO连接对象
  **/
    public function createConnection($dsn, array $config, array $options)
    {
        list($username, $password) = [
            $config['username'] ?? null, $config['password'] ?? null,
        ];

        try {
            return new PDO($dsn, $username, $password, $options);
        } catch (Exception $e) {
            return $this->tryAgainIfCausedByLostConnection(
                $e, $dsn, $username, $password, $options
            );
        }
    }
}

select方法

在connection中获取到数据库连接对象Connection之后,调用他的select方法,select方法中调用了run,run方法中先确保了pdo闭包存在(这个pdo闭包是在构造函数中传入的),然后调用闭包,这个闭包中完成了sql的执行,先通过getPdoForSelect方法调用pdo闭包,闭包中进行了PDO对象连接及一些基础配置的设置(具体参考数据库工厂中的createPdoResolver)并返回一个PDO对象,这里利用闭包做到了执行sql时才连接数据库。然后调用PDO的prepare方法,并在prepared中设置了PDOstatement的获取模式为FETCH_OBJ,绑定参数执行sql返回结果。

class Connection
{
  public function select($query, $bindings = [], $useReadPdo = true)
    {
      //$query = 'select * from user where name = ?';
      //$bindings = [1];
        return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
            if ($this->pretending()) {
                return [];
            }

           //在prepared中设置了返回结果的模式为FETCH_OBJ
            $statement = $this->prepared($this->getPdoForSelect($useReadPdo)
                              ->prepare($query)); //生成PODStatement对象

            $this->bindValues($statement, $this->prepareBindings($bindings)); //参数绑定

            $statement->execute(); //利用PDOStatement执行sql

            return $statement->fetchAll(); //获取sql的执行结果
        });
    }

    /**
* 确保pdo的存在并且执行闭包
**/
    protected function run($query, $bindings, Closure $callback)
    {
        //检查pdo连接是否存在
        $this->reconnectIfMissingConnection();

        $start = microtime(true);

        try {
            $result = $callback($query, $bindings);
        } catch (QueryException $e) {
            $result = $this->handleQueryException(
                $e, $query, $bindings, $callback
            );
        }
      $this->logQuery(
            $query, $bindings, $this->getElapsedTime($start)
        );

        return $result;
    }
}

查询构建器

上边我们了解了原生sql在laravel中的运行原理,我们在看看查询构建器是如何工作的,其实从字面意思猜他的作用大概就是构造sql语句。现在以DB::table('user')->where(['name' => 'test'])->get()这个为例。同原生sql运行一样,调用DatabasesManager中的__call生成一个数据库连接对象,调用数据库连接对象中的table方法。

DatabasesManager的table方法

这里的table比较简单,就是返回一个查询构建器并且设置他的属性from为'user'。

class Connection
{
  public function table($table)
    {
        return $this->query()->from($table);
    }

   public function query()
    {
      //生成一个查询构建器
        return new QueryBuilder(
            $this, $this->getQueryGrammar(), $this->getPostProcessor()
        );
    }
}

class QueryBuilder
{
  public function __construct(ConnectionInterface $connection,
                                Grammar $grammar = null,
                                Processor $processor = null)
    {
        $this->connection = $connection;
        $this->grammar = $grammar ?: $connection->getQueryGrammar();
        $this->processor = $processor ?: $connection->getPostProcessor();
    }

    public function from($table)
    {
        $this->from = $table;

        return $this;
    }
}

QueryBuilder中的where

where方法其实就是将我们传入的列名、操作符、值,存入数组中。供后续使用,当参数column为一个数组时,会先通过递归的方式将参数分解为where(‘列名’,'操作符','值')的形式。当我们传入\column = ['name'=>'test']这个时,会执行addArrayOfWheres方法,利用addArrayOfWheres中的闭包重新实例化一个查询构造器$query又一次调用where('name', '=', 'test'),$query->wheres = [ ['Basic', 'name', '=', 'test', 'and']],$query->bindings['where'] = [['test']],然后执行addNestedWhereQuery方法设置$this->wheres = [['Nested', $query, 'and']],$this->bindings['where'] = [['test']]

class QueryBuilder
{
 public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
        // 第一次  $column = ['name'=>'test'];$operator = null; $value = null; $boolean = 'and'
        // 第二次 $column = 'name', $operator = '=', $value='test', $boolean = 'and'
        if (is_array($column)) {
            return $this->addArrayOfWheres($column, $boolean);
        }
        
        //给$value, $operator赋值,对于省略的操作符赋值为‘=’
        list($value, $operator) = $this->prepareValueAndOperator(
            $value, $operator, func_num_args() === 2
        );

        if ($column instanceof Closure) {
            return $this->whereNested($column, $boolean);
        }

        //对于给出的一些非法操作符,默认替换为‘=’
        if ($this->invalidOperator($operator)) {
            list($value, $operator) = [$operator, '='];
        }
       
      //传入的值可以为回调函数这样就当作一个子查询来操作
        if ($value instanceof Closure) {
            return $this->whereSub($column, $operator, $value, $boolean);
        }

       
        if (is_null($value)) {
            return $this->whereNull($column, $boolean, $operator !== '=');
        }

       
        if (Str::contains($column, '->') && is_bool($value)) {
            $value = new Expression($value ? 'true' : 'false');
        }

        $type = 'Basic';
        //wheres[] = ['Basic', 'name', '=', 'test', 'and']
        $this->wheres[] = compact(
            'type', 'column', 'operator', 'value', 'boolean'
        );
        
        //将value添加到where的绑定数组中
        if (! $value instanceof Expression) {
            $this->addBinding($value, 'where');
        }

        return $this;
    }


  protected function addArrayOfWheres($column, $boolean, $method = 'where')
    {
      //$query = $this; $column = ['name'=>'test']; $method='where'; $boolean = 'and'
        return $this->whereNested(function ($query) use ($column, $method, $boolean) {
            foreach ($column as $key => $value) {
                if (is_numeric($key) && is_array($value)) {
                    //递归再调用where
                    $query->{$method}(...array_values($value));
                } else {
                    //这里又一次调用where方法$this->where('name','=','test','and')
                    $query->$method($key, '=', $value, $boolean);
                }
            }
        }, $boolean);
    }
}

public function whereNested(Closure $callback, $boolean = 'and')
    {
       //执行闭包
        call_user_func($callback, $query = $this->forNestedWhere());

        return $this->addNestedWhereQuery($query, $boolean);
    }

public function forNestedWhere()
    {
        //重新实例化一个查询构建器对象
        return new static($this->connection, $this->grammar, $this->processor)->from($this->from);
    }

public function addNestedWhereQuery($query, $boolean = 'and')
    {
        //$query->wheres = [['Basic', 'name', '=', 'test', 'and']]
        if (count($query->wheres)) {
            $type = 'Nested';
            //wheres[] = ['Nested', $query, 'and']
            $this->wheres[] = compact('type', 'query', 'boolean');
            //$query->bindings['where'] = [['test']] 
            //$this->bindings['where'] = [['test']]
            $this->addBinding($query->getRawBindings()['where'], 'where');
        }

        return $this;
    }

QueryBuilder中的get

在执行get时,会先通过Garmmer类(以后再具体来看这个类的实现)生成要执行的sql语句,作为数据库连接对象的select方法的参数执行sql,然后将返回结果通过Processor类处理之后使用Collection包裹起来返回。

class QueryBuilder
{
  public function get($columns = ['*'])
    {
        return collect($this->onceWithColumns($columns, function () {
            return $this->processor->processSelect($this, $this->runSelect());
        }));
    }

protected function onceWithColumns($columns, $callback)
    {
        $original = $this->columns;

        if (is_null($original)) {
            $this->columns = $columns;
        }

        $result = $callback();

        $this->columns = $original;

        return $result;
    }
protected function runSelect()
    {
      //实质还是调用了数据库连接器中的select
        return $this->connection->select(
            $this->toSql(), $this->getBindings(), ! $this->useWritePdo
        );
    }

public function toSql()
    {
      //通过Grammar类生成sql语句
        return $this->grammar->compileSelect($this);
    }
public function getBindings()
    {
      //将绑定数组转换为一维数组
        return Arr::flatten($this->bindings);
    }

}

数据库相关几个类之间的关系

名字 作用
DB DatabaseManager的一个服务代理
DatabaseManager 给外部提供的一个接口,外部调用数据库都走这个类
ConnectionFactory DatabaseManager的一个属性,DatabaseManager创建数据库连接对象的工具
Connection 数据库增删改查实现的地方
Connector 生成PDO的地方
Builder 连接Connection和Grammer,将Grammer生成的sql提供给Connection执行
Grammer 生成能执行的sql语句
Processor 处理Connection执行完sql的结果

Eloquent ORM

我们经常会写Model::find(1)或者(new Model)->find(1)但是model类中并找不到find方法,既然找不到又能运行一般都是魔术方法,接下来我们看看他是如何运行的。可以看出当调用一个不存在的静态方法时最终调用的都是__call方法。__call方法会先生成一个EloquentBuilder类,然后调用EloquentBuilder里的find方法。这篇先到这里吧,model的以后再补有点写不下去了。

class Model
{
  public function __call($method, $parameters)
    {
        if (in_array($method, ['increment', 'decrement'])) {
            return $this->$method(...$parameters);
        }

        return $this->newQuery()->$method(...$parameters);
    }

    public static function __callStatic($method, $parameters)
    {
        //当一个静态方法找不到时就当作非静态的方法调用
        return (new static)->$method(...$parameters);
    }

    public function newQuery()
    {
        return $this->registerGlobalScopes($this->newQueryWithoutScopes());
    }

public function newQueryWithoutScopes()
{
    return $this->newModelQuery()
                ->with($this->with)
                ->withCount($this->withCount);
}

public function newModelQuery()
{
    return $this->newEloquentBuilder(
        $this->newBaseQueryBuilder()
    )->setModel($this);
}

}

class EloquentBuilder
{
 public function find($id, $columns = ['*'])
    {
        if (is_array($id) || $id instanceof Arrayable) {
            return $this->findMany($id, $columns);
        }

        return $this->whereKey($id)->first($columns);
    }

public function whereKey($id)
    {
        if (is_array($id) || $id instanceof Arrayable) {
            $this->query->whereIn($this->model->getQualifiedKeyName(), $id);

            return $this;
        }

        return $this->where($this->model->getQualifiedKeyName(), '=', $id);
    }
public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
        if ($column instanceof Closure) {
            $column($query = $this->model->newModelQuery());

            $this->query->addNestedWhereQuery($query->getQuery(), $boolean);
        } else {
            $this->query->where(...func_get_args());
        }

        return $this;
    }
}
start=>start: 接受回调
end=>end: 返回结果
cond=>condition: 检查输入的参数
cond1=>condition: 检查结算单编号
op1=>operation: 查找系统中结算单号下的发票信息
cond2=>condition: 检查系统中发票数量<=传入的checkInvoices长度
cond3=>condition: 对比系统中每一个发票号码、编号、价税合计是否和传入的一致
op2=>operation: 将该结算单下的所有回调记录置为无效,并插入新纪录
start->cond
cond(yes)->op1->cond2
cond2(yes)->cond3
cond3(yes)->op2->end
cond2(no)->end
cond(no)->end

你可能感兴趣的:(数据库增删改查操作)