如何将数据库与模型分离并在多个表上扩展模型

时间:2017-07-15 17:05:30

标签: database design-patterns model-view-controller

我目前对模型使用自制的ORM方法,即对象的属性映射到表的字段。这似乎是一种非常可行和直观的方式来处理简单的网站。

...然而

随着复杂性的增加,当我想从连接表中获取数据时,这种方法变得很麻烦。我的数据库抽象仅针对单个表,并且使用连接进行查询并不会自然而然地落实到位。

我一直在调查DMM,特别是在https://github.com/codeinthehole/domain-model-mapper,但我在细节中某处迷路了。如果我能弄清楚我的问题是什么,我需要回答一些更大的问题:/

首先,MVC有一个很好的描述结构:模型,视图和控制器。您可以分离目录结构以分离关注点。但是,在我学习的过程中,模型并不是一个单一的类。您希望将存储与业务逻辑分开。

问题1:这些不同的模型类型是否有命名约定和目录层次结构?

第二个问题与实际的数据库访问有关。根据DMM模式,您可以通过调用->save()方法来保存内存中的对象。此内存中对象不一定与任何数据库表1:1映射。这就是我迷路的地方...... save方法可能会将内存中的对象注入到数据访问对象中,从而将对象保留在数据库中。

通过数据库抽象,我可以使用我自己的findfindallinsertupdatedelete等方法创建一个类。可以为每个数据库表扩展。但DMM似乎是一个完全不同的范例。是否有DAO的抽象,还是必须为每个应用程序定制设计?

问题2:如何将内存中对象映射到跨多个表的数据访问对象?

对于这两个问题,我意识到它们是抽象的问题。我不是要求别人为我调试代码;我想要理解它背后的理论。因此,很难提出易于回答问题的问题。但我欢迎任何部分答案,只要它能让社区有机会与我一起学习。

1 个答案:

答案 0 :(得分:1)

从一般角度来看,基于MVC概念的Web应用程序由两层组成:模型层和表示层。他们的实施实现了 - 关注点分离的目标。

model layer 由三个子图层组成:

  • domain layer (domain model) ,由域对象(内存中对象)组成 - 也称为模型。它们是封装业务逻辑的实体。因此,通过它们的结构和相互依赖,它们是现实世界(商业)对象/实体的抽象。该层还可以包含域对象集合之类的结构。
  • 存储层,由负责将域对象传入/传出底层存储系统的类组成(可能是RDBMS,会话,文件系统等):{{3} },repositories(data) mappers,数据访问抽象类(adaptersPDO - 及其包装器等等。这些结构的使用也达到了制作的目的域对象(完全)不可知,对存储类型及其处理方式不可知。
  • MySQLi 是根据执行涉及上两个子图层结构的操作的类(例如服务)构建的。例如,服务从存储系统中提取域对象,根据它的状态(属性)进行一些验证并返回相应的结果。

表示层包含:

  • 浏览即可。
  • 控制器即可。
  • [查看的模型

请注意,我没有完成此图层的说明。我是故意这样做的,因为我认为你更好地遵循这个链接,以便对这个主题有正确的看法:

关于第二个问题:实际上,ORM会自动化域层和数据库之间的映射。它们很有用,但也有缺点,因为它们迫使您在业务逻辑PLUS数据库结构方面进行思考。 " 每个表一个类"如(Model-View-Confusion Series)," protected $ tableName; "如在Table Data Gateway的父类Mapper中," 类用户扩展ActiveRecord "如DMM等,是灵活性限制的迹象。例如,正如我在DMM代码中看到的那样,它会强制您在Mapper构造函数中提供$tableName$identityFields。这是一个很大的限制。

无论如何,如果你想在涉及(复杂)查询数据库的任务中非常灵活,那么保持简单:

  • 保持域对象完全不知道存储系统。
  • 实施数据映射器模式,而不从任何父映射器继承特定映射器!在特定数据mapppers(保存,更新,插入,删除,查找,findByXXX等)的方法中,您可以使用纯SQL,无限复杂。阅读Active Record。当然,这样你就可以写更多的sql ......并成为一个SQL-virtuoso! :-)请注意任何其他"解决方案"会降低sql的灵活性。
  • 如果你真的需要从sql语言(SQL,T-SQL,PL / SQL等)中抽象出来,你甚至可以实现自己的查询构建器类并在数据映射器方法中使用它的实例,而不是sql语句。阅读PHP MVC: Data Mapper pattern: class design
  • 实现一个与PHP MVC: Query builder class for Data Mapper layer中类似的90%的Adapter类。 tableName之类的内容不应该出现在那里。
  • 创建PDO连接并将其注入Adapter对象的构造函数中。注意:不要像DMM那样在Adapter内部创建PDO,因为它们的适配器类紧密耦合到PDO。你应该 - 正确 - 松散耦合。您通过依赖注入实现了这一目标 - 请参阅DMM
  • 尝试使用依赖注入容器。像The Clean Code Talks - Don't Look For Things!一样。它将仅在应用程序的入口点(index.php或bootstrap.php)处理所有类的实例化和共享。最好的例子是PDO连接,通过Web MVC的完整周期共享。它非常强大,非常容易学习和使用,并且会让你的MVC变得非常苗条。首先关注Auryn

稍后您也想创建存储库和服务。

所以,关闭你的第一个问题:有一个非常好的解释文章系列,关于你感兴趣的内容。在你阅读它们之后,你将毫不怀疑所有模型层组件如何协同工作。注意:您会在那里看到与$tableName相同的属性,但现在您知道从哪个角度考虑它。所以:

这是一个映射器的版本,灵感来自上述文章。请注意,父/抽象类没有继承。要找出原因,请阅读An Introduction to Services中的答案。

数据映射器类:

<?php

/*
 * User mapper.
 * 
 * Copyright © 2017 SitePoint
 * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
 * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

namespace App\Modules\Connects\Models\Mappers;

use App\Modules\Connects\Models\Models\User;
use App\Modules\Connects\Models\Models\UserInterface;
use App\Modules\Connects\Models\Mappers\UserMapperInterface;
use App\Modules\Connects\Models\Collections\UserCollectionInterface;
use App\Core\Model\Storage\Adapter\Database\DatabaseAdapterInterface;

/**
 * User mapper.
 */
class UserMapper implements UserMapperInterface {

    /**
     * Adapter.
     * 
     * @var DatabaseAdapterInterface
     */
    private $adapter;

    /**
     * User collection.
     * 
     * @var UserCollectionInterface
     */
    private $userCollection;

    /**
     * 
     * @param DatabaseAdapterInterface $adapter Adapter.
     * @param UserCollectionInterface $userCollection User collection.
     */
    public function __construct(DatabaseAdapterInterface $adapter, UserCollectionInterface $userCollection) {
        $this
                ->setAdapter($adapter)
                ->setUserCollection($userCollection)
        ;
    }

    /**
     * Find user by id.
     * 
     * @param int $id User id.
     * @return UserInterface User.
     */
    public function findById($id) {
        $sql = "SELECT * FROM users WHERE id=:id";
        $bindings = [
            'id' => $id
        ];

        $row = $this->getAdapter()->selectOne($sql, $bindings);

        return $this->createUser($row);
    }

    /**
     * Find users by criteria.
     * 
     * @param array $filter [optional] WHERE conditions.
     * @return UserCollectionInterface User collection.
     */
    public function find(array $filter = array()) {
        $conditions = array();
        foreach ($filter as $key => $value) {
            $conditions[] = $key . '=:' . $key;
        }
        $whereClause = implode(' AND ', $conditions);

        $sql = sprintf('SELECT * FROM users %s'
                , !empty($filter) ? 'WHERE ' . $whereClause : ''
        );
        $bindings = $filter;

        $rows = $this->getAdapter()->select($sql, $bindings);

        return $this->createUserCollection($rows);
    }

    /**
     * Insert user.
     * 
     * @param UserInterface $user User.
     * @return UserInterface Inserted user (saved data may differ from initial user data).
     */
    public function insert(UserInterface $user) {
        $properties = get_object_vars($user);

        $columnsClause = implode(',', array_keys($properties));

        $values = array();
        foreach (array_keys($properties) as $column) {
            $values[] = ':' . $column;
        }
        $valuesClause = implode(',', $values);

        $sql = sprintf('INSERT INTO users (%s) VALUES (%s)'
                , $columnsClause
                , $valuesClause
        );
        $bindings = $properties;

        $this->getAdapter()->insert($sql, $bindings);

        $lastInsertId = $this->getAdapter()->getLastInsertId();

        return $this->findById($lastInsertId);
    }

    /**
     * Update user.
     * 
     * @param UserInterface $user User.
     * @return UserInterface Updated user (saved data may differ from initial user data).
     */
    public function update(UserInterface $user) {
        $properties = get_object_vars($user);

        $columns = array();
        foreach (array_keys($properties) as $column) {
            if ($column !== 'id') {
                $columns[] = $column . '=:' . $column;
            }
        }
        $columnsClause = implode(',', $columns);

        $sql = sprintf('UPDATE users SET %s WHERE id = :id'
                , $columnsClause
        );
        $bindings = $properties;

        $this->getAdapter()->update($sql, $bindings);

        return $this->findById($user->id);
    }

    /**
     * Delete user.
     * 
     * @param UserInterface $user User.
     * @return bool TRUE if user successfully deleted, FALSE otherwise.
     */
    public function delete(UserInterface $user) {
        $sql = 'DELETE FROM users WHERE id=:id';
        $bindings = array(
            'id' => $user->id
        );

        $rowCount = $this->getAdapter()->delete($sql, $bindings);

        return $rowCount > 0;
    }

    /**
     * Create user.
     * 
     * @param array $row Table row.
     * @return UserInterface User.
     */
    public function createUser(array $row) {
        $user = new User();
        foreach ($row as $key => $value) {
            $user->$key = $value;
        }
        return $user;
    }

    /**
     * Create user collection.
     * 
     * @param array $rows Table rows.
     * @return UserCollectionInterface User collection.
     */
    public function createUserCollection(array $rows) {
        $this->getUserCollection()->clear();
        foreach ($rows as $row) {
            $user = $this->createUser($row);
            $this->getUserCollection()->add($user);
        }
        return $this->getUserCollection()->toArray();
    }

    /**
     * Get adapter.
     * 
     * @return DatabaseAdapterInterface
     */
    public function getAdapter() {
        return $this->adapter;
    }

    /**
     * Set adapter.
     * 
     * @param DatabaseAdapterInterface $adapter Adapter.
     * @return $this
     */
    public function setAdapter(DatabaseAdapterInterface $adapter) {
        $this->adapter = $adapter;
        return $this;
    }

    /**
     * Get user collection.
     * 
     * @return UserCollectionInterface
     */
    public function getUserCollection() {
        return $this->userCollection;
    }

    /**
     * Set user collection.
     * 
     * @param UserCollectionInterface $userCollection User collection.
     * @return $this
     */
    public function setUserCollection(UserCollectionInterface $userCollection) {
        $this->userCollection = $userCollection;
        return $this;
    }

}

数据映射器接口:

<?php

/*
 * User mapper interface.
 */

namespace App\Modules\Connects\Models\Mappers;

use App\Modules\Connects\Models\Models\UserInterface;

/**
 * User mapper interface.
 */
interface UserMapperInterface {
    /**
     * Find user by id.
     * 
     * @param int $id User id.
     * @return UserInterface User.
     */
    public function findById($id);

    /**
     * Find users by criteria.
     * 
     * @param array $filter [optional] WHERE conditions.
     * @param string $operator [optional] WHERE conditions concatenation operator.
     * @return UserCollectionInterface User collection.
     */
    public function find(array $filter = array(), $operator = 'AND');

    /**
     * Insert user.
     * 
     * @param UserInterface $user User.
     * @return UserInterface Inserted user (saved data may differ from initial user data).
     */
    public function insert(UserInterface $user);

    /**
     * Update user.
     * 
     * @param UserInterface $user User.
     * @return UserInterface Updated user (saved data may differ from initial user data).
     */
    public function update(UserInterface $user);

    /**
     * Delete user.
     * 
     * @param UserInterface $user User.
     * @return bool TRUE if user successfully deleted, FALSE otherwise.
     */
    public function delete(UserInterface $user);

    /**
     * Create user.
     * 
     * @param array $row Table row.
     * @return UserInterface User.
     */
    public function createUser(array $row);

    /**
     * Create user collection.
     * 
     * @param array $rows Table rows.
     * @return UserCollectionInterface User collection.
     */
    public function createUserCollection(array $rows);
}

适配器类:

<?php

namespace App\Core\Model\Storage\Adapter\Database\Pdo;

use PDO;
use PDOStatement;
use PDOException as Php_PDOException;
use App\Core\Exception\PDO\PDOException;
use App\Core\Exception\SPL\UnexpectedValueException;
use App\Core\Model\Storage\Adapter\Database\DatabaseAdapterInterface;

abstract class AbstractPdoAdapter implements DatabaseAdapterInterface {

    /**
     * Database connection.
     * 
     * @var PDO
     */
    private $connection;

    /**
     * Fetch mode for a PDO statement. Must be one of the PDO::FETCH_* constants.
     * 
     * @var int
     */
    private $fetchMode = PDO::FETCH_ASSOC;

    /**
     * Fetch argument for a PDO statement.
     * 
     * @var mixed 
     */
    private $fetchArgument = NULL;

    /**
     * Constructor arguments for a PDO statement when fetch mode is PDO::FETCH_CLASS.
     * 
     * @var array 
     */
    private $fetchConstructorArguments = array();

    /**
     * For a PDOStatement object representing a scrollable cursor, this value determines<br/>
     * which row will be returned to the caller.
     * 
     * @var int 
     */
    private $fetchCursorOrientation = PDO::FETCH_ORI_NEXT;

    /**
     * The absolute number of the row in the result set, or the row relative to the cursor<br/>
     * position before PDOStatement::fetch() was called.
     * 
     * @var int 
     */
    private $fetchCursorOffset = 0;

    /**
     * @param PDO $connection Database connection.
     */
    public function __construct(PDO $connection) {
        $this->setConnection($connection);
    }

    /**
     * Fetch data by executing a SELECT sql statement.
     * 
     * @param string $sql Sql statement.
     * @param array $bindings [optional] Input parameters.
     * @return array An array containing the rows in the result set, or FALSE on failure.
     */
    public function select($sql, array $bindings = array()) {
        $statement = $this->execute($sql, $bindings);
        $fetchArgument = $this->getFetchArgument();
        if (isset($fetchArgument)) {
            return $statement->fetchAll(
                            $this->getFetchMode()
                            , $fetchArgument
                            , $this->getFetchConstructorArguments()
            );
        }
        return $statement->fetchAll($this->getFetchMode());
    }

    /**
     * Fetch the next row from the result set by executing a SELECT sql statement.<br/>
     * The fetch mode property determines how PDO returns the row.
     * 
     * @param string $sql Sql statement.
     * @param array $bindings [optional] Input parameters.
     * @return array An array containing the rows in the result set, or FALSE on failure.
     */
    public function selectOne($sql, array $bindings = array()) {
        $statement = $this->execute($sql, $bindings);
        return $statement->fetch(
                        $this->getFetchMode()
                        , $this->getFetchCursorOrientation()
                        , $this->getFetchCursorOffset()
        );
    }

    /**
     * Store data by executing an INSERT sql statement.
     * 
     * @param string $sql Sql statement.
     * @param array $bindings [optional] Input parameters.
     * @return int The number of the affected records.
     */
    public function insert($sql, array $bindings = array()) {
        $statement = $this->execute($sql, $bindings);
        return $statement->rowCount();
    }

    /**
     * Update data by executing an UPDATE sql statement.
     * 
     * @param string $sql Sql statement.
     * @param array $bindings [optional] Input parameters.
     * @return int The number of the affected records.
     */
    public function update($sql, array $bindings = array()) {
        $statement = $this->execute($sql, $bindings);
        return $statement->rowCount();
    }

    /**
     * Delete data by executing a DELETE sql statement.
     * 
     * @param string $sql Sql statement.
     * @param array $bindings [optional] Input parameters.
     * @return int The number of the affected records.
     */
    public function delete($sql, array $bindings = array()) {
        $statement = $this->execute($sql, $bindings);
        return $statement->rowCount();
    }

    /**
     * Prepare and execute an sql statement.
     * 
     * @todo I want to re-use the statement to execute several queries with the same SQL statement 
     * only with different parameters. So make a statement field and prepare only once!
     * See: https://www.sitepoint.com/integrating-the-data-mappers/
     * 
     * @param string $sql Sql statement.
     * @param array $bindings [optional] Input parameters.
     * @return PDOStatement The PDO statement after execution.
     */
    protected function execute($sql, array $bindings = array()) {
        // Prepare sql statement.
        $statement = $this->prepareStatement($sql);

        // Bind input parameters.
        $this->bindInputParameters($statement, $bindings);

        // Execute prepared sql statement.
        $this->executePreparedStatement($statement);

        return $statement;
    }

    /**
     * Prepare and validate an sql statement.<br/>
     * 
     * ---------------------------------------------------------------------------------
     * If the database server cannot successfully prepare the statement,
     * PDO::prepare() returns FALSE or emits PDOException (depending on error handling).
     * ---------------------------------------------------------------------------------
     * 
     * @param string $sql Sql statement.
     * @return PDOStatement If the database server successfully prepares the statement,
     * return a PDOStatement object. Otherwise return FALSE or emit PDOException 
     * (depending on error handling).
     * @throws Php_PDOException
     * @throws PDOException
     */
    private function prepareStatement($sql) {
        try {
            $statement = $this->getConnection()->prepare($sql);
            if (!$statement) {
                throw new PDOException('The sql statement can not be prepared!');
            }
        } catch (Php_PDOException $exc) {
            throw new PDOException('The sql statement can not be prepared!', 0, $exc);
        }
        return $statement;
    }

    /**
     * Bind the input parameters to a prepared PDO statement.
     * 
     * @param PDOStatement $statement PDO statement.
     * @param array $bindings Input parameters.
     * @return $this
     */
    private function bindInputParameters($statement, $bindings) {
        foreach ($bindings as $key => $value) {
            $statement->bindValue(
                    $this->getInputParameterName($key)
                    , $value
                    , $this->getInputParameterDataType($value)
            );
        }
        return $this;
    }

    /**
     * Get the name of an input parameter by its key in the bindings array.
     *  
     * @param int|string $key The key of the input parameter in the bindings array.
     * @return int|string The name of the input parameter.
     */
    private function getInputParameterName($key) {
        return is_int($key) ? ($key + 1) : (':' . ltrim($key, ':'));
    }

    /**
     * Get the PDO::PARAM_* constant, e.g the data type of an input parameter, by its value.
     *  
     * @param mixed $value Value of the input parameter.
     * @return int The PDO::PARAM_* constant.
     */
    private function getInputParameterDataType($value) {
        $dataType = PDO::PARAM_STR;
        if (is_int($value)) {
            $dataType = PDO::PARAM_INT;
        } elseif (is_bool($value)) {
            $dataType = PDO::PARAM_BOOL;
        }
        return $dataType;
    }

    /**
     * Execute a prepared PDO statement.
     * 
     * @param PDOStatement $statement PDO statement.
     * @return $this
     * @throws UnexpectedValueException
     */
    private function executePreparedStatement($statement) {
        if (!$statement->execute()) {
            throw new UnexpectedValueException('The statement can not be executed!');
        }
        return $this;
    }

    /**
     * Get the ID of the last inserted row or of the sequence value.
     * 
     * @param string $sequenceObjectName [optional] Name of the sequence object<br/>
     * from which the ID should be returned.
     * @return string The ID of the last row, or the last value retrieved from the specified<br/>
     * sequence object, or an error IM001 SQLSTATE If the PDO driver does not support this.
     */
    public function getLastInsertId($sequenceObjectName = NULL) {
        return $this->getConnection()->lastInsertId($sequenceObjectName);
    }

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

    public function setConnection(PDO $connection) {
        $this->connection = $connection;
        return $this;
    }

    public function getFetchMode() {
        return $this->fetchMode;
    }

    public function setFetchMode($fetchMode) {
        $this->fetchMode = $fetchMode;
        return $this;
    }

    public function getFetchArgument() {
        return $this->fetchArgument;
    }

    public function setFetchArgument($fetchArgument) {
        $this->fetchArgument = $fetchArgument;
        return $this;
    }

    public function getFetchConstructorArguments() {
        return $this->fetchConstructorArguments;
    }

    public function setFetchConstructorArguments($fetchConstructorArguments) {
        $this->fetchConstructorArguments = $fetchConstructorArguments;
        return $this;
    }

    public function getFetchCursorOrientation() {
        return $this->fetchCursorOrientation;
    }

    public function setFetchCursorOrientation($fetchCursorOrientation) {
        $this->fetchCursorOrientation = $fetchCursorOrientation;
        return $this;
    }

    public function getFetchCursorOffset() {
        return $this->fetchCursorOffset;
    }

    public function setFetchCursorOffset($fetchCursorOffset) {
        $this->fetchCursorOffset = $fetchCursorOffset;
        return $this;
    }

}

关于您的第一个问题:没有关于您应该在哪里存储课程的约定。选择您希望的任何文件系统结构。但请确保:

1)您正在使用PHP MVC: Data Mapper pattern: class design中建议的自动加载器和命名空间。

2)您可以随时唯一标识每个组件类。您可以通过两种方式实现此目的:通过对每个类(UserControllerUserMapperUserView等)应用相应的后缀,或者在{{1中定义相应的类别名语句,如:

use

文件系统结构可能类似于以下内容 - 它是我项目中使用的文件系统结构,如果它乍一看太复杂,那就很抱歉:

namespace App\Controllers; use App\Models\DomainObjects\User; use App\Models\Mappers\User as UserMapper; use App\Models\Repositories\User as UserRepository;

PSR-4 Autoloading Standard

App/Core

enter image description here

祝你好运!