Back-End

2 mai, 2018

Versão alfa de acesso antecipado do Atlas.ORM “Cassini” (v3)

Publicidade

Para aqueles que não sabem, Atlas é um ORM para seu modelo de persistência, não para seu modelo de domínio. O Atlas 1 “Albers” (para PHP 5.6) foi lançado em abril de 2017. O Atlas 2 “Boggs” (para PHP 7.1) foi lançado em outubro de 2017.

E agora, em abril de 2018, temos uma versão de acesso antecipado do Atlas 3 “Cassini”, o produto de várias lições de alguns anos de uso.

Arquitetura e composição

Uma grande mudança arquitetônica é que o Atlas 3 não usa mais os pacotes Aura mais antigos para conexões e consultas SQL; em vez disso, o Atlas adotou esses pacotes como seus e os atualizou para o PHP 7.1 usando digitação restrita e retornos anuláveis. Outra grande mudança arquitetônica é que as implementações do gateway de dados da tabela e do mapeador de dados estão agora disponíveis como pacotes por direito próprio.

O resultado final é um mini-framework, construído a partir de uma pilha de pacotes, onde qualquer pacote “inferior” pode ser usado independentemente dos que estão “acima”. A hierarquia de pacotes de baixo para cima se parece com isso:

  • Atlas.Pdo: Descendente do Aura.Sql, ela fornece um objeto Connection do banco de dados e um ConnectionLocator. Se tudo o que você precisa é um conveniente wrapper em torno do PDO com métodos de busca e produção, este é o pacote para você.
  • Atlas.Query: Descendente de Aura.SqlQuery, ela é um construtor de consultas que envolve uma Connection Atlas.Pdo. Se você quiser apenas criar e executar consultas SQL usando uma abordagem orientada à objetos, os objetos de consulta Atlas podem lidar com isso.
  • Atlas.Table: Extraído do Atlas 2, trata-se de uma implementação de gateway de dados de tabela que usa o Atlas.Query por debaixo dos panos. Se você não precisa de um sistema completo de mapeamento de dados e deseja apenas interagir com tabelas individuais e seus objetos de linha, esta fará o truque.
  • Atlas.Mapper: Também extraído do Atlas 2, esta é uma implementação do mapeador de dados que modela as relações entre tabelas. Ela permite que você crie objetos Record e RecordSet cujos objetos Row mapeiam naturalmente de volta aos objetos Table. Você pode escrevê-las de volta ao banco de dados, uma a uma, ou persistir um gráfico inteiro do Record de volta no banco de dados de uma só vez.
  • Atlas.Orm: Finalmente, no topo da hierarquia, o pacote ORM abrangente fornece um envoltório de conveniência em torno do sistema Mapper e fornece várias estratégias para o gerenciamento de transações.

Existem também dois pacotes “laterais”:

  • Atlas.Info: Descendente de Aura.SqlSchema, ele inspeciona o banco de dados para obter informações sobre tabelas, colunas e sequências.
  • Atlas.Cli: Este pacote de interface de linha de comando usa o Atlas.Info para examinar o banco de dados e, em seguida, cria classes de esqueleto para seu modelo de persistência a partir das tabelas do banco de dados.

Separação, conclusão e hierarquia

Uma meta do Atlas 3 era dividir os subsistemas Table e Mapper em seus próprios pacotes, para que pudessem ser usados no lugar do ORM transacional completo, se necessário. Junto a isso, eu queria um melhor autocompletar de IDE, especialmente no nível de construção de consultas, particularmente no suporte a diferentes dialetos de SQL (por exemplo, o PostgreSQL tem uma cláusula Returning, mas nada mais que isso).

A primeira ideia dessas linhas era ter pacotes paralelos, um para cada driver SQL: Atlas.Query-Mysql, Atlas.Query-Pgsql, Atlas.Query-Sqlite, etc. No entanto, percebi que o typehinting em níveis mais altos teria sido um problema. Se uma classe de Table genérica retorna um TableSelect que estende um objeto Select genérico, em seguida, fornecer um MysqlSelect específico do driver tinha que retornar um MysqlTableSelect para um MysqlTable. Isso, por sua vez, significaria pacotes paralelos para cada driver até a hierarquia: Table, Mapper e Orm. Os genéricos no nível da linguagem podem ter resolvido isso, mas o PHP não os tem, logo, essa ideia estava fora.

Então o plano era ter um único pacote ORM “mestre” com todos os subsistemas incluídos nele como pacotes virtuais, e métodos de template onde implementações específicas de driver poderiam ser preenchidas. No entanto, isso acabou sendo uma abordagem de tudo ou nada, onde os pacotes de nível “inferior” não podiam ser usados independentemente dos pacotes de níveis “mais altos”.

Eu poderia pensar em apenas uma outra alternativa que permitiria o preenchimento automático de IDE para a funcionalidade específica do driver e mantivesse os pacotes bem separados. Isso foi para tornar o Atlas.Query mais polivalente. Como resultado, o método returning() está disponível, mesmo que o banco de dados de back-end específico não o reconheça.

Não estou especialmente feliz com isso, pois prefiro que as classes exponham apenas a funcionalidade específica do driver, mas a desvantagem é que você obtém a conclusão completa do IDE. (Eu racionalizo isso dizendo que os objetos de consulta não estão lá para impedir que você escreva SQL incorreto, apenas para permitir que você escreva SQL com métodos de objetos; você poderia enviar uma cláusula Returning para o MySQL, colocando-a em uma string de consulta. Novamente, os genéricos no nível do PHP ajudariam aqui).

Além disso, nos objetos de consulta, me vi querendo executar a consulta representada pelo objeto diretamente desse objeto, em vez de passá-lo manualmente pelo PDO toda vez. Dessa forma, os objetos de consulta agora pegam uma instância PDO (que é decorada automaticamente por uma Connection Atlas.Pdo) para que você possa fazer coisas como esta:

$result = Select::new($pdo)
    ->columns('foo', 'bar')
    ->from('table_name')
    ->where('baz = ', $baz)
    ->fetchAll();

Gateways, Mappers e relacionamentos

Com isso, a próxima etapa foi extrair o subsistema de gateway de dados da tabela para seu próprio pacote separado. A nova biblioteca Atlas.Table não é muito diferente da versão Atlas 2. A maior mudança é que a funcionalidade do mapa de identidade foi movida “para cima” uma camada para o sistema Mapper. Isso mantém o pacote mais de acordo com as expectativas para implementações de gateway de dados de tabela.

Isso, por sua vez, levou à extração do subsistema do mapeador de dados para seu próprio pacote também. O Atlas.Mapper agora é responsável pelo mapeamento de identidade de Rows que servem como o núcleo de cada Record, mas o trabalho de gerenciamento de transações foi movido em uma camada para o pacote ORM abrangente.

De particular interesse, o novo pacote Atlas.Mapper elimina a ideia de uma relação “muitos-para-muitos” como um elemento de primeira classe. Descobriu-se que gerenciar muitos-para-muitos relacionados era contra-intuitivo e contraproducente de formas sutis, mas significativas, e não menos importante era ter que acompanhar registros novos ou deletados em dois lugares (a associação “direta” e o “estrangeiro”, lado mais distante da associação).

Por debaixo dos panos, o Atlas 2 tinha que carregar os registros de mapeamento de associação de qualquer maneira, forçando uma chamada explícita with() ao buscar um relacionamento de muitos-para-muitos através do mapeamento de associação parecia ser uma abordagem razoável. Com isso, você só precisa gerenciar o mapeamento de associação para adicionar ou remover os registros “estrangeiros” no lado distante do relacionamento.

Também no departamento de relacionamento, o relacionamento “ManyToOneByReference” foi renomeado para “ManyToOneVariant”. (Eu acho que o nome flui melhor).

Finalmente, os relacionamentos 1: 1 e 1: M agora suportam diferentes tipos de funcionalidade de exclusão em cascata. Esses métodos na definição de relacionamento terão esses efeitos no registro externo quando o registro nativo for excluído:

Finalmente, os relacionamentos 1: 1 e 1: M agora suportam diferentes tipos de funcionalidade de exclusão em cascata. Esses métodos na definição de relacionamento terão esses efeitos no registro externo quando o registro nativo for excluído:

  • onDeleteSetNull(): Define as chaves $foreignRecord para null.
  • onDeleteSetDelete(): Chama $foreignRecord::setDelete() para marcá-lo para exclusão.
  • onDeleteCascade(): Chama $foreignMapper::delete ($foreignRecord) para deletar imediatamente.
  • onDeleteInitDeleted(): Presume que o banco de dados excluiu o registro externo por meio do acionador e reinicializa a linha do registro externo em Deleted.

Além disso, o relacionamento 1:M desanexará registros excluídos de RecordSets estrangeiros, e o relacionamento 1:1 definirá o valor do registro estrangeiro como false quando excluído. Isso ajuda a gerenciar os objetos na memória, para que você não precise desanexar ou remover os registros excluídos você mesmo.

Gerenciamento de transações ORM

No topo da hierarquia do framework, temos o pacote Atlas.Orm. Quase todas as funcionalidades foram extraídas para os pacotes subjacentes; as únicas partes restantes são: objeto Atlas abrangente, com seus métodos convenientes, e o sistema de gerenciamento de transações.

Enquanto o Atlas 2 forneceu algo semelhante a uma implementação da Unidade de Trabalho, cheguei à conclusão de que a Unidade de Trabalho é um padrão de camada de domínio, não um padrão de camada de persistência.

Trata-se de acompanhar as alterações nos objetos de domínio e gravá-las de volta no banco de dados de maneira apropriada: “Uma unidade de trabalho registra tudo o que você faz durante uma transação comercial que pode afetar o banco de dados. Quando você termina, descobre tudo o que precisa ser feito para alterar o banco de dados como resultado do seu trabalho. ”(Cf. POEAA: UnitOf Work).

Com isso em mente, o Atlas 3 fornece estratégias de gerenciamento de transações mais típicas, embora com alguma automação, se você achar que precisa:

  • A estratégia padrão AutoCommit inicia no modo “autocommit”, o que significa que cada interação com o banco de dados é sua própria micro-transação (cf. https://secure.php.net/manual/en/pdo.transactions.php). Você pode, é claro, iniciar manualmente/submeter/reverter transações como desejar.
  • A estratégia do AutoTransact iniciará automaticamente uma transação quando você executar uma operação de gravação e, em seguida, confirmar automaticamente essa operação ou revertê-la na exceção. (Este foi o padrão para o Atlas 2).
  • A estratégia BeginOnWrite iniciará automaticamente uma transação quando você executar uma operação de gravação. Não irá se comprometer ou reverter; você precisará fazer isso sozinho. Depois disso, na próxima vez que você executar uma operação de gravação, o Atlas iniciará outra transação.

Finalmente, a estratégia BeginOnRead iniciará automaticamente uma transação quando você executar uma operação de gravação ou uma operação de leitura. Não irá se comprometer ou reverter; você precisará fazer isso sozinho. Depois disso, na próxima vez que você executar uma operação de gravação ou leitura, Atlas iniciará outra transação.

Estilo e abordagem

Mesmo quando eu era o líder no PSR-1 e no PSR-2, comecei a me afastar um pouco deles enquanto uso mais o PHP 7. Em particular, acho que as coisas se alinham melhor quando você coloca a chave de abertura na próxima linha, para um método com um retorno digitado e uma lista de argumentos que se espalha por várias linhas. Isto é, em vez disso:

    public function foo(
        LongClassName $foo,
        LongerClassName $bar
    ) : ?VeryLongClassName {
        // ...
    }

Estou me inclinando a isso:

 public function foo(
        LongClassName $foo,
        LongerClassName $bar
    ) : ?VeryLongClassName
    {
        // ...
    }

O bit extra de espaço em branco nesta situação proporciona uma boa separação visual.

Também estou começando a me afastar um pouco da minha dedicação às interfaces. Um dos problemas com a exposição de interfaces é que a compatibilidade retroativa rígida torna-se mais difícil de manter, sem também sobrecarregar as principais versões; se você achar que precisa de apenas um novo recurso, não será possível adicioná-lo à interface sem quebrar o BC.

Como tal, para manter ambos os Versões Semânticas e para evitar a adição de métodos fora da interface em versões sub-principais, o Atlas 3 evita as interfaces inteiramente a favor de implementações de classes abstratas.

Além disso, essas classes abstratas não usam o prefixo “Resumo”, como fiz no passado. Isso significa que o Atlas pode digitar nas classes Abstratas sem que pareça feio, e pode evitar quebras técnicas de BC quando interfaces são adicionadas. Isso também tem seus próprios problemas; não é uma solução, mas uma compensação.

Eu também estou tentando evitar docblocks, tanto quanto possível, em favor de typehinting tudo que for possível.

Atualizando do Atlas 2 para o Atlas 3

Considerando que o caminho de atualização do Atlas 1 para o Atlas 2 foi relativamente simples, o salto do Atlas 2 para o Atlas 3 é muito maior. As alterações mais significativas estão nos nomes e assinaturas dos métodos nos níveis Conexão e Consulta; as classes Table agora têm suas próprias Row em vez de usar uma linha genérica para todos os propósitos; as classes Mapper agora separam a lógica de definição de relacionamento também para sua própria classe. Eu prepararei um documento de atualização para ajudar a facilitar a transição, mas se você está começando a usar o Atlas, tente primeiro a v3 “Cassini”.

Você está preso a um aplicativo legado do PHP? Você pode comprar meu livro. Ele oferece um guia passo a passo para melhorar sua base de código, tudo isso enquanto a mantém em execução o tempo todo.

***

Paul M. Jones faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://paul-m-jones.com/archives/6883