📜 ⬆️ ⬇️

About design patterns for working with RDBMS

Introduction


Working with an RDBMS is one of the most important parts of developing web applications. Discussions about how to properly present data from the database in the application has been going on for a long time. There are two main patterns for working with databases: ActiveRecord and DataMapper. ActiveRecord is considered by many anti-pattern programmers. It is argued that ActiveRecord objects violate the principle of a single duty (SRP). DataMapper is considered to be the only correct approach to ensuring persistence in OOP. The first part of the article is devoted to the fact that DataMapper is far from ideal both conceptually and in practice. The second part of the article shows how you can improve your code using existing ActiveRecord implementations and a few simple rules. The submission relates mainly to transactional RDBMS.


About the inferiority of DataMapper or the mismatch of the real declared


Apologists for DataMapper note that this pattern provides an opportunity to abstract from the database and program in "terms of business objects". It is prescribed to create abstract storages in which so-called. "Entities" are objects containing persistent application data. In fact, it is proposed to emulate the database in the application, creating object storage over the RDBMS. Allegedly, this should allow to achieve separation of business logic from the database. However, in any serious application, operations on multiple records are required. Their implementation in the form of working with objects is usually much less efficient than SQL queries. As a solution to this problem, it is proposed to do a part of the code in terms of objects, and where this fails, use SQL or some own query language translated to SQL (HQL, DQL). The idea does not work fully, therefore, it is proposed to actually return to SQL.


Entities, despite the absence of SQL code inside, are still dependent on the database. This is especially evident when programming links (one entity is declared the main one, the other subordinate). Relational relationships somehow flow into the object structure. In fact, entities are no longer “business objects”, but “passive records”. Moreover, these are not objects at all, but data structures that must be processed by a special converter object for storage and retrieval from the database. This is especially noticeable in CRUD applications. Entities in such applications degenerate into data containers without any functionality and are known as anemic entities. As a solution, it is proposed to put business logic in essence. This statement is also in doubt. First, entities in CRUD applications will remain anemic. Nowhere to take business logic to fill empty classes. DataMapper does not work in CRUD applications. Secondly, business logic almost always needs dependencies. Rarely, what real business logic will work only on the data of the entity itself. The correct way to get dependencies is through the designer. However, most implementations of DataMapper limit the design, making the implementation of the constructor unavailable. The use of embedding a method as a replacement for embedding a constructor makes an object inferior. Such an object cannot do anything by itself, it always needs to transfer all the necessary dependencies. As a result, the client code is contaminated with ubiquitous dependency transmission.


The most famous implementation of DataMapper in PHP is Doctrine ORM. To use this library, you need either annotations or additional files that define the mapping. The first method well shows the connection between the entity and the database, even if it is implicit. The transformation itself is based on the use of the Reflection API. Data is placed and retrieved without any participation of the object itself - work with the object is carried out as with the data structure. Obviously, this violates encapsulation - one of the basic principles of OOP. API Doctrine ORM is quite voluminous, quite a lot of pitfalls. It takes time to learn how to effectively use this library. All of the above applies to other implementations of DataMapper to varying degrees. Given the above arguments, DataMapper seems redundant, especially since it still does not relieve knowledge of SQL and RDBMS, it does not give any real independence from the database. Code that uses Doctrine ORM will generally remain forever attached to it.


Using ActiveRecord


Practically each of the popular PHP frameworks offers its own way of working with databases. Most use their own implementation of ActiveRecord. As a rule, for the sake of speed of developing generic applications, ActiveRecord is not only responsible for interacting with the database, but also the role of a business object, a validator, and sometimes a form. The problems of such use of ActiveRecord are well known and well described in many articles. As a solution, it is usually proposed to rewrite all code using DataMapper. This article proposes the use of ActiveRecord, which relieves some of the responsibilities by following a few simple rules.


The solution is described further with examples of pseudocode. Some calls may not be properly formulated, the purpose of the article is to show an idea, and not a concrete working implementation. Constructors and some obvious methods, as well as some checks are omitted for brevity. As the AR implementation, Yii is used. This framework is chosen for examples because there are a lot of projects written on it that need to be refactored, supported, they need to be considered.


The approach is applicable to other frameworks and independent implementations of ActiveRecord. First, the code that is applicable to projects that are completely dependent on Yii will be shown. It is quite simple. Further examples will be shown with dependency injection and using Yii as a library for implementing interfaces of objects interacting with a database.


Insert and modify data


First you need to create a class that describes the gateway object to one or a group of related database tables. In this article, such classes are called repositories, but they do not work like repositories in DataMapper. The object representing the record or records in the database should not be used in the absence of data in the database itself. A repository must be used for insertion and creation.


class YiiARUserRepository
{
    public function add(string $email, string $name, array $phones, DateTimeInterface $created_at)
    {
        return $this->transaction->call(function () use($email, $name, $phones, $created_at) {

            //      email,   UI          
            $ar = new YiiARUser([
                'email'      => $email,
                'name'       => $name,
                'created_at' => $created_at->format('Y-m-d H:i:s')
            ]);
            $ar->insert();
            foreach ($phones as $phone) {
                $ar->addPhone($phone['phone'], $phone['description']);
            }

            return $ar;
        });

    }

}

class YiiDbTransaction
{

    public function call(callable $callable)
    {
        $txn = \Yii::$app->db->beginTransaction();
        try {

            $result = $callable();

            $txn->commit();

            return $result;

        } catch (\Exception $e) {
            $txn->rollback();
            throw $e;
        }
    }

}

class YiiARUser extends yii\db\ActiveRecord
{
    //...
    public function addPhone(string $phone, string $description)
    {
        $ar = new YiiARPhone([
            'user_id'     => $this->id,
            'phone'       => $phone,
            'description' => $description
        ]);
        $ar->insert();

        return $ar;
    }

}

, , . . beforeSave() . , . — . Symfony, Doctrine DDD , , — UUID. — , .


class YiiARUser extends yii\db\ActiveRecord
{
    //...
    public function changePassword(string $password)
    {
        $this->updateAttributes([
            'password' => $this->security->hash($password)
        ]);
    }

    public function rename(string $name)
    {
        $this->updateAttributes([
            'name' => $name
        ]);
    }

}

class RegisterForm
{
    public function register(DateTimeInterface $created_at): YiiARUser
    {

        if ( ! $this->validate()) {
            throw new \DomainException($this->errorMessage());
        }

        return $this->transaction->call(function () use($created_at) {
            $user = $this->user_repo->add($this->email, $this->name, [], $created_at);
            $user->changePassword($this->password);
            $user->changeSomething($some_data);
            foreach ($this->phone_forms as $form) {
                $user->addPhone($form->phone, $form->description);
            }

            return $user;
        });

    }
}

, , . . AR , AR save() . Yii WithRelatedBehavior. . "Active" ActiveRecord , , . - " " " " .


, AR. , , .


. INSERT UPDATE . , , , , YiiARUser::changeInfo($phones, $addresses, $name, $email).



. . Yii - Yii . , AR. — .


class YiiARUserRepository
{
    //...
    public function findOne($id)
    {
        return YiiARUser::findOne($id);
    }

    public function findUsersWithGroups($limit)
    {
        return YiiARUser::find()->with('groups')->all();
    }

    //   DataProvider,       Yii
    public function findAll(): DataProviderIterface
    {
        //...
    }

    //   
    public function findAll(): \Iterator
    {
        //...
        return new class($reader) implements \Iterator
        {
            //...
            public function current()
            {
                $data = $this->reader->current();

                return YiiARUser::instantiate($data);
            }
        }
    }

}

, DataProvider data widgets (RAD- Yii). , Yii.



, . , . .


//        
$user->with_related_behavior->setAttributes($request->post());

// array_diff(), AR::isNewRecord()     ,   
//,     temporal coupling
$user->with_related_behavior->save();

. , setAttributes() , , save() . , . . , , . UI, HTTP .


class UserUpdateForm
{

    public function update(YiiARUser $user)
    {

        $this->transaction->call(function () use ($user) {

            //...
            foreach ($this->changedPhones() as $phone)
                $user->changePhone($phone['id'], $phone['phone'], $phone['description'])

            $user->addPhones($this->addedPhones());

        });

    }

}

class YiiARUser extends yii\db\ActiveRecord
{

    //...

    public function changePhone(int $phone_id, $phone, $description)
    {
        $phone = YiiARPhone::findOne(['id' => $phone_id, 'user_id' => $this->id]);
        if ($phone == null) {
            throw new \DomainException('  .');
        }
        $phone->updateAttributes([
            'phone'       => $phone,
            'description' => $description
        ]);
    }

    public function addPhones($phones)
    {
        YiiARUser::$db->createCommand()->barchInsert('phones', ['phone', 'description'], $phones)->execute();
    }

}

, . , Yii resetRelation($name). , , ( ), .


, — . , — . . . .


public function addPhones(array $phones)
{

    $this->transaction->call(function () {

        $id = YiiARUser::$db->query('SELECT id FROM users FOR UPDATE;')->queryScalar();

        if ($id === null) {
            throw new \DomainException('  .');
        }

        if ($this->phoneCount() + count($phones) > 5) {
            throw new \DomainException('  !');
        }

        YiiARUser::$db->createCommand()->batchInsert('phones', ['phone', 'description'], $phones);

    });

}

Doctrine ORM , .



, .


$user->delete();

class YiiARUser extends yii\db\ActiveRecord
{

    public function delete()
    {
        self::$db->createCommand()->delete('phones', ['user_id' => $this->id]);
        $this->delete();
        //    ,        (  )
    }

}

DataMapper :


//     :
$user->delete();

// :
$em->delete($user);

, DataMapper — . — ( ).


,


, / , . . Yii-.
-, SQL-, , . , AR. , , , . , , , , . , legacy- .


interface UserRepository
{

    public function add(string $name, string $email, array $phones, \DateTimeInterface $created_at): User;

    public function findOne($id);

}

interface User
{

    public function addPhones($phones);

    public function rename($name);

    public function changePassword($pwd);

}

class YiiDbUserRepository
{

    public function add(string $name, string $email, array $phones, \DateTimeInterface $created_at): User
    {
        $ar = $this->transaction->call(function () use($name, $email, $phones, $created_at) {

            $ar = new YiiARUser([
                'name'       => $name,
                'email'      => $email,
                'created_at' => $created_at->format('Y-m-d H:i:s')
            ]);
            $ar->addPhones($phones);

            return $ar;

        });

        return new YiiDbUser($ar);

    }

    public function findOne($id)
    {
        $ar = YiiARUser::findOne($id);
        if ($ar === null) {
            return null;
        }

        return new YiiDbUser($ar);

    }

}

class YiiDbUser implements User
{

    private $ar;

    public function addPhones(array $phones)
    {
        //multiple insert command
    }

    public function rename(string $name)
    {
        //   
        if ($this->ar->name !== $name) {
            $this->ar->updateAttributes(['name' => $name]);
        }
    }

    public function changePassword(string $pwd)
    {
        $this->ar->updateAttributes(['password' => $this->hasher->hash($pwd)]);
    }

    public function phones(): \Iterator
    {
        // YiiARUser  Yii-  YiiARPhone
        $phone_ars = $this->ar->phones;

        $iterator = new ArrayIterator($phone_ars);

        return new class($iterator, $this->dependency) implements \Iterator
        {

            //...
            public function current()
            {
                $ar = $this->iterator->current();

                // YiiDbPhone e  YiiARPhone
                return new YiiDbPhone($ar, $this->dependency);
            }

        }

    }

}

, AR, . , .


class YiiARUser extends \yii\db\ActiveRecord implements User
{
  //...
}

, , , . . . — . , . - Yii . , AR DataProvider, RAD- . , , Yii AR. , , AR Yii . - — .



, , . . , . . .


, . Author AuthorRepository.
.


interface Post
{

    public function id(): int;

    public function title(): string;

    public function author(): Author;

    public function authorId(): int;

}

interface PostRepository
{
    public function findAllWithAuthors(int $limit): array;
}

class YiiARPost extends \yii\db\ActiveRecord
{
  //...  
}

class YiiDbPostRepository implements PostRepository
{

    private $author_repository;

    public function findAllWithAuthors(int $limit): \Iterator
    {
        $ars = YiiARPost::findAll(['limit' => $limit]);

        $iterator = new \ArrayIterator($ars);

        $ids = [];

        foreach ($ars as $ar) {

            $ids[] = $ar->id;

        }

        $authors = $this->author_repository->findAll($ids);

        return new class($iterator, $this->author_repository, $authors) implements \iterator
        {

            private $iterator;

            private $author_repository;

            private $authors;

            //...
            public function current()
            {
                $ar = $this->iterator->current();

                return new AuthoredPost(
                    new YiiDbPost($ar, $this->author_repository),
                    $this->authors
                );
            }

        }

  }

}

class YiiDbPost implements Post
{

    private $ar;

    private $author_repository;

    public function title(): string
    {
        return $this->ar->title();
    }

    public function content(): string
    {
        return $this->ar->content();
    }

    public function author(): Author
    {
        return $this->author_repository->findOne($this->ar->author_id);
    }

    public function authorId(): int
    {
        return $this->ar->id;
    }

}

class AuthoredPost implements Post
{

    private $post;

    private $authors;

    public function title(): string
    {
        return $this->post->title();
    }

    public function content(): string
    {
        return $this->post->content();
    }

    public function author(): Author
    {

        foreach ($this->authors as $author) {
            if ($author->id() == $this->post->authorId()) {
                return $author;
            }
        }
        throw new DomainException('  !   !');

    }

}

interface Author
{

    public function id(): int;

    public function name(): string;

}

interface AuthorRepository
{

    public function fundOne(int $id);

    public function findAll(array $ids): array;

}

AuthoredPost — . — . . .


class UserAuthor implements Author
{

    private $user;

    public function id(): int
    {
        return $this->user->id();
    }

    public function name(): string
    {
        return $this->user->name();
    }

}

class UserAuthorRepository implements AuthorRepository
{

    private $repository;

    public function findOne(int $id)
    {

        $user = $this->repository->findOne($id);

        if ($user === null) {
            return null;
        }

        return new UserAuthor($user);

    }

    public function findAll(array $ids): \Iterator
    {
        $users = $this->repository->findAll($ids);

        return new class($users) implements \Iterator
        {

            //..
            public
            function current()
            {
                $user = $this->iterator->current();

                return new UserAuthor($user);
            }

        };
    }

}

PHP — Yii . . . . — . .


JOIN , . . , . " — ", . . — , . , .


-


-, — . , .


class SomeLogicUser
{

    private $user;

    //...

    public function doSomething()
    {

        $name = $this->calculateName();

        //  
        $this->transaction->call(function () {
            $user->rename($name);
            $user->changeSomething($data);
        });

        //    -,     -   
        $user->changeEverythingRequiredUsingOneMethod($name, $data);

    }

}

DataMapper


, (Unit of Work). DataMapper , , -. - (, final). . , , Reflection API. . final, .


, SQL-. , . .


MySQL, , Unit of Work Doctrine ORM . — / .



. //, SQL, , . . . , DataMapper Unit of Work , , SQL , API, , , . . , . , .


')

Source: https://habr.com/ru/post/339582/


All Articles