5. Banco de dados

Banco de dados (Mysql)

Tudo bem, este artigo é um pouco grande, nós entendemos. Mas é realmente necessário que você leia e compreenda tudo, para assim poder ter certeza de que está fazendo seu aplicativo funcionar corretamente. Valos lá, você já leu artigos maiores e muito menos interessantes...

Até este momento nós incluímos no SuitUp somente o banco de dados mais famoso do mundo (MySql da Oracle). Nós não temos absolutamente nada contra nenhum outro tipo de banco de dados, muito pelo contrário, as opções são muitas e cada um tem seus pontos fortes a oferecer. Talvez você queira utilizar outro tipo de banco de dados e pense que por isso não é possível utilizar o SuitUp, mas a verdade é que com poucas adaptações é possível integrar outro módulo de banco de dados. Mesmo assim não vamos entrar neste assunto sem antes existir demanda pra isso.

O que há nesta página


Conectando com o Banco de Dados

Antes de começar a utilizar seu banco de dados via SuitUp você precisa incluir ao projeto as configurações para realizar a conexão com ele. Para isso nós fizemos [[este manual|5.1 database.config.php]] que irá explicar tudo que você precisa fazer para realizar esta conexão.

[[Manual para conexão com seu banco de dados MySql|5.1 database.config.php]]

Estrutura

Dentro de cada módulo deve haver uma pasta Model com a seguinte estrutura

1
2
3
4
5
6
7
8
ModuleName
|  Model
|  |  Gateway
|  |  |  User.php
|  |  SqlFiles (opcional)
|  |  |  user
|  |  |  |  getById.sql
|  |  UserBusiness.php

Os arquivos demonstrados na estrutura acima são representativos, ou seja, servem apenas de exemplos.

Business (Regras de Negócio)

Esta é uma estrutura interessante de projeto que evita problemas como perder ou pular uma regra de negócio, como? Um controlador nunca deve acessar um Gateway, esta é a classe que efetiva as consultas no banco de dados. Em vez disso um controlador ou qualquer outro lugar do sistema deve acessar sempre um Business (BO, BusinessObject) que deve retornar resultados do banco de dados, mas aplicando regras de negócio.

Para entender melhor, vamos supor que você tenha uma regra de negócio em seu projeto que ao excluir um usuário também apaga todos os pedidos feito por ele. Se você acessar o Gateway diretamente para excluir este usuário ele será excluído e deixará os pedidos sem serem excluídos, dessa forma você garante que a regra de negócio será sempre aplicada. Claro que o programador pode pular esta estrutura se desejar, esta é a maneira que nós recomendamos.

IMPORTANTE: Nunca deixe um Business chamar um método de outro Gateway que não é o dele, isto também seria uma quebra de regras de negócio. Business só se comunica com Business

Criando um Business (BO)

Lembre-se, cada tabela do banco de dados deve ter sua Business e sua Gateway. Para criar a Business de uma tabela basta estendê-la de \SuitUp\Database\Business\AbstractBusiness e criar um atributo protected de nome $gateway. Os métodos ficam por sua necessidade.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace ModuleName\Model

use \SuitUp\Database\Business\AbstractBusiness;

class UserBusiness extends AbstractBusiness
{
    /**
     * Atributo obrigatório  
     * @var Gateway\User
     */
    protected $gateway;

    /**
     * EXEMPLO.
     * Retorna o nome do usuário.
     * 
     * @param int $userId ID do usuário
     * @return string
     */
    public function userNameById($userId) {
        // Note aqui a regra de negócio sendo aplicada.
        $result = $this->gateway->getUserById($userId);
        return $result['name'];
    }
}

Métodos padrão de Business

Todas as Business vêm por padrão com os seguintes métodos. São ações muito comuns de serem necessárias que não precisam ser reescritas. Lembre-se de que para que esses métodos funcionem é necessário também criar a respectiva classe Gateway para a mesma tabela.

INSERT - $someBo->insert(array $data);

Este método realiza somente o INSERT no banco de dados e retorna o ID (last insert ID) gerado para a tabela.

Exemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Apenas um exemplo
try {

    // Insert
    $userBo = new \ModuleDefault\Model\UserBusiness();
    $newId = $userBo->insert(array(
        'name' => 'Braghim Sistemas',
        'email' => 'braghim.sistemas@gmail.com',
    ));
} catch (\Exception $e) {
    // Error control
}


UPDATE - $someBo->update(array $data, array $where, $noWhereForSure = false);

Este método realiza somente o UPDATE no banco de dados e retorna o número de linhas alteradas com isso. O parâmetro $noWhereForSure ativado (true) significa que o parâmetro $where foi passado vazio de propósito, pois isso significa que o UPDATE irá alterar todos os registros da tabela, o que não é de modo algum algo comum.

Exemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Apenas um exemplo
try {

    $data = array(
        'name' => 'Braghim Sistemas',
        'email' => 'braghim.sistemas@gmail.com',
    );

    // Update
    $userBo = new \ModuleDefault\Model\UserBusiness();
    $newId = $userBo->update($data, array('pk' => 1));

} catch (\Exception $e) {
    // Error control
}


DELETE - $someBo->delete(array $where);

Este método remove efetivamente um registro do banco de dados e retorna o número de linhas afetadas.

Exemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Apenas um exemplo
try {

    // Update
    $userBo = new \ModuleDefault\Model\UserBusiness();
    $newId = $userBo->delete(array('pk' => 1));

} catch (\Exception $e) {
    // Error control
}
Caso você tenha necessidade de fazer delete lógico, ou seja, utilizar status para identificar registros removidos em vez de removê-los de fato será necessário implementar seu próprio método.


GET - $someBo->get('PK 1');

Este método irá retornar o item da tabela do banco de dados que tem este(s) ID('s). Você verá mais adiante que ao criar um Gateway você deverá indicar o(s) ID('s) da tabela, assim o SuitUp sabe quais registros procurar.

Se sua tabela trabalha com chaves compostas (varias colunas como chave primária) será necessário passar todas elas como parâmetros separados.

Exemplo:

1
2
3
// Apenas um exemplo
$userBo = new \ModuleDefault\Model\UserBusiness();
$user = $userBo->get(1, 'active');


SAVE - $someBo->save(array());

Este método identifica se é necessário fazer um insert ou update no banco de dados automaticamente, serve na realidade como apelido para ambos. Para identificar qual método o SuitUp irá utilizar, este método verifica se no array existem as chaves primárias da tabela e se existirem faz um update, caso contrário será um insert.

Exemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Apenas um exemplo
try {

    // Insert
    $userBo = new \ModuleDefault\Model\UserBusiness();
    $newId = $userBo->save(array(
        'name' => 'Braghim Sistemas',
        'email' => 'braghim.sistemas@gmail.com',
    ));

    // Update
    $userBo = new \ModuleDefault\Model\UserBusiness();
    $userBo->save(array(
        'pk' => $newId,
        'name' => 'Braghim Sistemas',
        'email' => 'braghim.sistemas@gmail.com',
    ));

} catch (\Exception $e) {
    // Error
}


Gateway

São os gateways que vão de fato realizar as consultas no banco de dados, você pode criar queries (SQL) por Objeto, String e até mesmo por arquivos (.sql).

Criando um Gateway

Um Gateway deve obrigatoriamente conter os atributos $name e $primary onde o primeiro deve ser uma string com o nome da tabela (pode conter o nome do banco como db_main.tb_user por exemplo) e o segundo pode ser uma string com o nome da PK, mas preferencialmente um array com a lista dele(s).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
namespace ModuleDefault\Model\Gateway

use \SuitUp\Database\Gateway\AbstractGateway;

class User extends AbstractGateway {

    /**
     * Defina aqui o nome da tabela e o nome da(s) chave(s) primária(s).
     * Isto é obrigatório.
     */
    protected $name = 'user';
    protected $primary = array('id_user');

    /**
     * Opcionalmente você pode indicar uma coluna que a cada update receberá um update
     * automático sem que você precise indicar na chamada do método update();
     */
    protected $onUpdate = array('edit' => 'NOW()');

    /**
     * Sql por Objeto. Este é o método que nós recomendamos em primeiro lugar,
         * mais seguro e mais fácil de dar manutenção
     */
    public function getUserByIdObject($userId) {
        $sql = $this->select("SELECT * FROM user")
            ->where("pk_user = ?", $userId);

        return $this->db->row($sql);
    }

    /**
     * Sql por String.
     */
    public function getUserByIdString($userId) {
        $sql = "SELECT * FROM user WHERE pk_user = :user";

        return $this->db->row($sql, array(
            'user' => $userId
        ));
    }

    /** Utilize este apenas em queries pequenas e simples.
     * Arquivo SQL que vai estar em ModuleDefault/Model/SqlFiles/user/getById.sql
         * este caminho é encontrado automaticamente pelo caminho até o model.
     */
    public function getUserById($userId) {
        $sql = $this->sqlFile('getById');

        return $this->db->row($sql, array(
            'user' => $userId
        ));
    }
}

Montar consultas complexas

Ao montar uma consulta mais complexa nós recomendamos utilizar os objetos como $this->select('SELECT * FROM ...');. Esta é a forma mais segura e mais fácil de dar manutenção depois, além também de facilitar para montar a consulta quando esta tem questões condicionais como filtros.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
namespace ModuleDefault\ModelMain\Gateway;

use SuitUp\Database\Gateway\AbstractGateway;

class Phone extends AbstractGateway
{
    protected $name = 'maindb.tb_phone'; // database + table
    protected $primary = array('id_phone');

    /**
     * Lista os telefones de um usuario
     * 
     * @param int $idRegion
     * @param array $filters
     * @return \SuitUp\Paginate\Paginate
     */
    public function listphonesByRegion($idRegion, array $filters = array()) {

        // Subquery retorna o número de atendentes em cada telefone da regiao
        $subQuery = $this->select("SELECT COUNT(1) FROM otherdb.tb_operator op1")
            ->where('p.id_phone = op1.id_phone')
            ->where('op1.status = 1');

        $query = $this->select("SELECT p.* FROM {$this->name} as p")
            ->columns(array(
                'p.id_phone',
                'p.country',
                'p.area_code',
                'p.number',
                'r.id_region',
                'r.state',
                'r.city',
                'r.neighborhood',
                "($subQuery)" => 'qtdOps', // Subquery
            ))
            ->innerJoin("maindb.tb_region as r", "r.id_region = p.id_region")
            ->where('r.id_region = ?', $idRegion)
            ->where('r.status = 1')
            ->where("($subQuery) > 0") // Subquery > 0
            ->order("u.`name` ASC");

        // Filtro por regiao (estado)
        if (isset($filters['state']) && $filters['state']) {
            $query->where('UPPER(r.state) LIKE UPPER(?)', '%'.$filters['state'].'%');
        }

        // Retornando dados paginados
        return $this->db->paginate($query);
    }
}

Métodos para consultas SQL

De fato, para quem está habituado a escrever consultas para banco de dados estes métodos seguintes dispensam qualquer tipo de apresentação.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public function columns(array $columns)

public function innerJoin($table, $onClause)

public function leftJoin($table, $onClause)

public function rightJoin($table, $onClause)

public function where($where, $value = null, $type = null)

public function orWhere($where, $value = null, $type = null)

public function group($column)

public function order($column)

public function having($text);

public function limit($limit, $offset = null);

Tipos de retorno de dados

É possível escolher como a consulta vai retornar resultados, em array para várias linhas ao mesmo tempo, apenas uma linha, apenas um valor sem nome ou até mesmo em pares do tipo id => valor. São esses métodos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Retorna lista com várias linhas do banco de dados (de acordo com a consulta)
$this->db->query($query, $params = null, $fetchmode = \PDO::FETCH_ASSOC);

// Retorna apenas as colunas indicadas por número (começando por 0 [zero])
$this->db->column($query, $params = null);

// Retorna apenas a primeira linha do resultado segundo a consulta
$this->db->row($query, $params = null, $fetchmode = \PDO::FETCH_ASSOC);

// Retorna um único resultado sem nem mesmo nome de coluna
$this->db->single($query, $params = null);

Os métodos acima são aqueles que nós julgamos que sejam mais necessários e utilizados para o dia-a-dia, mas é possível escolher o \PDO::FETCH_* necessário que o PHP disponibiliza aqui. Neste caso utilize da seguinte forma:

1
$this->db->query($query, $params = null, $fetchmode = \PDO::FETCH_ASSOC);

Arquivos SQL

Recomendado para consultas simples

Para montar a consulta por meio de arquivo SQL você deve simplesmente seguir as regras SQL para a instrução desejada, nós pegamos o conteúdo deste arquivo, substituímos os valores e rodamos ela no banco de dados exatamente como você a montou. O que complica é que quando você precisar adicionar instruções condicionalmente como filtros fica muito difícil fazer isso simplesmente com SQL e sem programação, por isso indicamos este método de arquivos .sql apenas para consultas simples.

Os parâmetros da consulta devem ser precedidos do :

1
SELECT * FROM user u WHERE u.pk_user = :user

Transações

A qualquer momento você pode iniciar uma transação no banco de dados. As transações fazem com que as ações realizadas no escopo depois de begin só tenham efeito após um commit, ou seja, se houver um rollBack nada será alterado no banco. É uma forma de garantir a persistência dos dados e não realizar ações pela metade, causando assim inconsistência nos dados do banco. Veja abaixo um exemplo na prática.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
\SuitUp\Database\Database::beginTransaction();
try {

    $phoneBo = new \ModuleDefault\ModelLogs\PhoneBusiness();
    $newPhone = $phoneBo->insert(array(
        'country' => '+55',
        'area_code' => '85',
        'number' => '555-5555',
    ));

    // Erro absurdo
    if ($newPhone%2 == 0) {
        throw new \Exception("Arbitrary error");
    }

    // Não caiu no erro absurdo, agora sim o banco vai gravar este valor
    \SuitUp\Database\Database::commit();

} catch(\Exception $e) {

    // Nada foi alterado
    \SuitUp\Database\Database::rollBack();
}