Salvando objetos com ID definido no Data Mapper (CI)

Desde que comecei a estudar e utilizar o Code Igniter eu recomendo o uso do ORM Data Mapper, que facilita a vida nas consultas ao banco de dados (leia: tutorial do Data Mapper no Code Igniter), esta semana ele mais uma vez se mostrou muito útil enquanto eu fazia a migração do banco antigo do And After para o novo.

Depois de dominar (yeaaaah) relacionamento de tabelas com o Code Igniter (usando o Data Mapper) e finalizar o sistema do And After neste framework estou fazendo a migração de todo o conteúdo para a nova plataforma: textos, comentários, cadastro de usuários, logs, tags, blogs…

Achei que seria extremamente trabalhoso, pois toda a estrutura de banco foi alterada – recomecei o sistema do zero sem pensar na migração, fiz isso para evitar "puxadinhos" e gambiarras (que com o crescimento de sistema, estava cheio) e desenvolver tudo direito, do zero, um sistema novo.

No ônibus voltando para São Paulo fiz em alguns minutos a migração dos usuários e iniciei a dos textos (que será um pouco mais complicada devido aos relacionamentos), mas tive um problema com o Data Mapper: não perder os ID"s dos objetos que estão sendo migrados.

Como salvar um objeto com um ID existente?

O Data Mapper usa o atributo ID do objeto para verificar se ele deve fazer um insert ou um update, se o objeto ID existe ele fará um UPDATE, portanto você não consegue setar o ID de um objeto e usar o $object->save();, pois ele fará um update mesmo que o objeto não exista (e não vai acontecer nada no seu banco).

Para isso o Data Mapper tem a função save_as_new, segue um exemplo abaixo:

$user = new User();
$user->id = 1;
$user->login = "Admin";
$user->password = "password";
$success = $user->save_as_new();

Se não deu nenhum problema no seu banco (neste caso ele deveria estar limpo) você acabou de criar um usuário "Admin" com o id 1na sua tabela.

Problemas comuns

Após fazer a migração pode acontecer problemas ao tentar salvar novos objetos na tabela, isso pode acontecer porque o auto_increment (do ID) não foi atualizado – e está tentando inserir um novo objeto na tabela com um ID que já foi registrado "na mão".

Para facilitar o entendimento vamos a ao exemplo do caso da migração do And After. Existem aproximadamente 1.500 usuários cadastrados no site, após a migração o último ID utilizado na tabela de usuários foi o 1600 (devido a usuários excluídos, testes no banco, etc).

Preciso avisar a minha tabela que o próximo auto_increment (ID) é 1601, e não 1 (que seria o primeiro inserido de forma "natural", sem ID setado). O código abaixo executa esta atualização no banco:

// Update MySQL AUTO_INCREMENT:
$user->db->query("ALTER TABLE `users` AUTO_INCREMENT = 1601;");

Agora a migração da tabela users está completa e é possível utilizar ela normalmente.

Para mais dúvidas sobre o save leia a documentação do data mapper (EN).

Relacionamento de tabelas no Data Mapper (CI)

Se você não entendeu muito bem o post explicando como melhorar as recomendações de posts relacionados com o Data Mapper, este tutorial poderá ajudar.

Criei um material para apoiar a documentação do Data Mapper sobre o relacionamento de tabelas no banco de dados. Se você não está entendendo nada, recomendo que antes de continuar você leia:

  1. Usando o Data Mapper no Code Igniter – como instalar e configurar o Data Mapper
  2. Entendo o relacionamento de banco de dados

 

Nomenclatura das tabelas relacionais no Data Mapper

O primeiro passo para fazer o relacionamento de tabelas funcionar é estruturar o banco de dados para isso. Para ilustrar este post vamos criar um relacionamento "one to many" de usuários para posts.

Na nossa estrutura do banco de dados temos a tabela users e a tabela posts.

Por regra do Data Mapper as tabelas de relacionamento devem ser estruturadas da seguinte forma:

  1. Nome da primeira tabela do relacionamento (ordem alfabética)
  2. Underline "_"
  3. Nome da segunda tabela do relacionamento

No nosso caso temos as tabelas "users" e "posts", portanto nossa tabela de relacionamento que deve ser criada no banco de dados se chamará "posts_users" e terá três colunas:

id - INT, auto increment

user_id - INT

post_id - INT

Pronto, com esta tabela criada no nosso banco de dados podemos configurar nossas classes "User" e "Post" no Data Mapper.

 

Configurando as classes para relacionamento

Continuando o exemplo anterior da classe User, vamos adicionar a configuração de relacionamento, ela ficará assim:

class User extends DataMapper {
    var $table = "users";

    var $has_many= "posts, comments";

    function User()
    {
        parent::DataMapper();
    }
}

E agora nossa clase Post:

class Post extends DataMapper {
    var $table = "posts";

    var $has_one= "user";
    var $has_many= "comments";

    function User()
    {
        parent::DataMapper();
    }
}

 

Pronto, suas tabelas estão relacionadas pelo Data Mapper e você pode usufruir as facilidades do ORM para criar "innerjoins" que eu não entendo!

 

Salvando dados de relacionamento

Para salvar um relacionamento é simples, criamos os dois objetos que vamos relacionar e usamos o método save do Data Mapper.

É mais fácil exemplificar do que explicar:


$u = new User();
$u->where("login", "odesenvolvedor")->get();

$p = new Post();
$p->title = "Novo post"; $p->content = "Conteúdo html do nosso post"

// Salvamos o novo post e relacionamos ao nosso User
$p->save($u);

Para relacionar dois objetos já existentes a lógica é a mesma:

$u = new User();
$u->where("login", "odesenvolvedor")->get();

$p = new Post();
$p->
where("title", "Novo Post"); $p->get();

// Relacionamos o Post que já existia ao nosso User
$p->save($u);

Este é o básico do relacionamento de dados utilizando o Data Mapper. Para entender recomendo a leitura da documentação do DM.

Para um exemplo prático mais avançado veja como selecionar posts mais relevantes através da comparação de tags.

Selecionando posts relacionados pelas tags (Code Igniter com Data Mapper)

Desenvolvendo o backend da nova versão do And After resolvi aprimorar o sistema de recomendações de posts relacionados do site e torná-lo mais relevante para os leitores.

Para acompanhar este post é necessário entender sobre relacionamento no banco de dados, saber um pouco como funciona o Data Mapper, a ferramenta ORM e também ter uma noção de relacionamento de tabelas com o Data Mapper.

Posts relacionados – Como funciona hoje?

Atualmente o sistema pega as tags de um post e, uma a uma, busca outros posts que utilizem a mesma tag. Estes posts são exibidos (sem repetição) e com um limite estipulado.

Isso significa que as últimas tags do post que o leitor está lendo podem ser ignoradas – pois possivelmente com as primeiras tags o sistema de relacionamento já tem posts suficientes para uma lista de recomendação.

 

Como deveria funcionar?

Para "afinar" o sistema de recomendação o ideal é levar em consideração todas as tags de dois posts que podem ser similares e ver quantas tags eles tem em comum. Quanto mais tags em comum, mais relacionado está o texto – porém não podemos esquecer de levar em consideração a data de publicação do mesmo, para evitar ficar exibindo apenas posts muito antigos.

 

Tecnologias

Estou utilizando PHP com o framework Code Igniter (tudo bonitinho com MVC) e banco de dados MySQL, a ferramenta ORM utilizada é o Data Mapper (leia também: como instalar e configurar o Data Mapper no Code Igniter).

Para atingir o objetivo criei a seguinte regra de negócio:

  • Percorrer cada tag que o post atual tem e buscar os posts recentes que contém esta mesma tag.
  • Atribuir um "ponto" para cada vez que um post é encontrado
  • Ordenar a lista de posts relacionados pelos "pontos" (número de vezes que um post retornou como relacionado do atual)

 

Vamos ao código! A função abaixo recebe duas variáveis: $post (o id do post que o usuário está lendo) e $limit (quantos posts relacionados devem retornar no array de resultado).

function getRelatedPost($post, $limit) {
  //Pega o post atual e suas tags
  $p = new Post();
  $p->select("id");
  $p->where("id", $post);
  $p->get(1);
  $tags = $p->tag->get();
  $arr = array();
  //Percorre as tags do post atual
  foreach($tags as $t){
    //pega os posts relacionados pela tag
    $related = new Post();
    $related->where_related_tag("id", $t->id);
    $related->where("id <>", $post);
    $related->order_by("creationDate DESC");
    $related->get($limit);
    //Se existirem posts, percorre cada post relacionado para esta tag
    if(!empty($related->id)){
      foreach($related as $pr){
        //Se o post já existir no array soma mais um ponto para ele
        //Se ele não existir cria ele no array com valor 1
        $id = $pr->id;
        if(!empty($arr[$id])){
          $arr[$id] = ($arr[$id]+1);
        }else{
          $arr[$id] = 1;
        }
      }
    }
  }
  //Ordena o array pelos pontos
  //e pega cada post relacionado para retornar como resultado
  arsort($arr, SORT_REGULAR);
  $relArr = array();
  foreach ($arr as $i => $value) {
    $relArr[] = $this->getPostById($i);
  }
  return($relArr);
}
 
 
 
Lembrando que estou utilizando a versão OverZealous do Data Mapper, que foi desenvolvida como um upgrade da versão padrão desta ferramenta.
 
E agora o que rola por trás deste código todo, que o Code Igniter retorna no benchmark para os curiosos de plantão:
 
 
SELECT `tags`.*
FROM (`tags`)
LEFT OUTER JOIN `posts_tags` as posts_tags ON `tags`.`id` = `posts_tags`.`tag_id`
LEFT OUTER JOIN `posts` as posts ON `posts`.`id` = `posts_tags`.`post_id`
WHERE `posts`.`id` = 8 
 
 
SELECT `posts`.*
FROM (`posts`)
LEFT OUTER JOIN `posts_tags` as posts_tags ON `posts`.`id` = `posts_tags`.`post_id`
LEFT OUTER JOIN `tags` as tags ON `tags`.`id` = `posts_tags`.`tag_id`
WHERE `tags`.`id` = 14
AND `posts`.`id` <> 8
ORDER BY `posts`.`creationDate` DESC
LIMIT 5 

 

Qualquer dúvida, correção ou melhoria no código comente! 🙂

Entendendo o relacionamento de tabelas no banco de dados

Dando continuidade ao tutorial do Data Mapper para Code Igniter hoje vou escrever sobre um assunto que sempre complicou minha vida quando o assunto é MySQL: relacionamento de tabelas.

Este post explicará apenas o básico sobre relacionamento, quando finalizei ele tratando de relacionamento e do uso de relacionamentos no Data Mapper ele acabou bastante extenso, optei por quebrar o post em dois para facilitar a leitura.

Se você já entende de relacionamentos não perca tempo com este post, aguarde o tutorial específico de como usar relacionamentos no Data Mapper para Code Igniter (será linkado neste post assim que for publicado!).

 

O que é relacionamento de tabelas?

Primeiro vamos entender um pouco sobre o funcionamento e tipos de relacionamento, deixando de lado o Data Mapper e falando apenas da questão lógica do negócio.

O relacionamento existe quando um ou mais dados de uma tabela estão relacionados de alguma forma com um ou mais dados de outra tabela.

Por exemplo, temos uma tabela d usuários (users) e uma tabela de posts (posts), cada usuário pode publicar infinitos posts porém cada post poderá ter apenas um usuário. Estas tabelas estão relacionadas.

Existe também relacionamento de dados de uma tabela com outros dados desta mesma tabela. Um usuário (user) pode ter vários amigos da mesma tabela (user), então os dados estão relacionados com dados da mesma tabela.

Agora vamos aos tipos de relacionamento:

 

Relacionamento um para um (one to one)

Neste tipo de relacionamento um dado de uma tabela equivale a um dado em outra tabela exatamente.

Por exemplo um usuário (table users) está relacionado a um endereço na tabela adress, e cada endereço só está relacionado a um usuário.

 

Relacionamento um para muitos – One to Many

No relacionamento um para muitos um dado da tabela um pode estar relacionado a diversos dados da tabela dois, porém cada dado da tabela dois estão relacionados a apenas um dado da tabela um.

Por exemplo um user (table users) pode estar relacionado a diversas casas (table houses), porém cada casa só está relacionada a um user.

 

Relacionamento muitos para muitos – Many to many

No "many to many" os dados da primeira tabela podem estar relacionados a diversos dados da segunda tabela e os dados da segunda tabela também podem estar relacionados a diversos dados da primeira tabela.

Exemplo: um usuário pode ter diversas habilidades (user com diversos relacionamentos para a tabela skills) e cada habilidade também pode estar relacionada a diversos usuários (dado da tabela skill relacionado a diversos dados da tabela users).

 

 

Estrutura das tabelas para relacionamento

Agora que já conhecemos os tipos de relacionamento, vamos entender como estruturar nosso banco de dados para permitir o acesso fácil aos dados relacionados.

Quando o relacionamento é um para um ou um para muitos podemos estruturar as colunas diretamente na tabelas relacionadas, continuando com o exemplo da tabela users e posts, na nossa tabela posts  podemos ter uma coluna chamada user_id  que identifica a qual user está relacionado aquele post.

Table users

id | name | email

 

Table posts

id | title | content | user_id

 

No exemplo acima temos automaticamente um relacionamento um para muitos de users->posts e um para um de posts->users.

Quando o relacionamento é muitos para muitos a estrutura do exemplo acima não é viável (a não ser com uma boa gambiarra, com split de valores e performance porca como eu já fiz). A solução é "quebrar" o relacionamento e inserí-lo em uma nova tabela destinada somente a isso.

Vamos supor que temos uma tabela images onde ficam armazenadas as imagens relacionadas aos posts. Um post poderá ter diversas images e uma image poderá ser utilizada em múltiplos posts. Para fazer este relacionamento criamos uma tabela de relacionamento, que para ilustrar este exemplo chamaremos de images_posts e terá a estrutura abaixo:

TABLE IMAGES_POSTS

id | image_id | post_id

 

Com este tipo de estruturação podemos relacionar infinitos posts com infinitas images. Por exemplo, o post com id = 4 poderá ter as imagens com id = 8, id = 9 e id = 10.

Teremos:

TABLE IMAGES_POSTS

id | image_id | post_id

1  | 8                | 4

2  | 9                | 4

3  | 10              | 4

 

Pronto, estrutura feira e as images 8, 9 e 10 também poderão estar relacionadas a outros posts também, apenas inserindo mais dados nesta tabela de relacionamento.

 

O próximo post explicará como inserir, acessar e manipulas dados de tabelas com relacionamento utilizando a ferramenta ORM Data Mapper para Code Igniter!

Espero que este post seja uma boa referência para estudos, para dar continuidade ao aprendizado veja alguns livros sobre MySQL que podem te ajudar.

Usando o Data Mapper no Code Igniter

Este post nasceu da necessidade de explicar um pouco mais o funcionamento do Data Mapper para o framework Code Igniter, para ficar mais fácil fazers posts mais avançados utilizando esta ótima ferramenta.

O que é Object-relational mapping (ORM)?

ORM é uma técnica que facilita o acesso e manipulação de banco de dados "transformando-os" como se fossem objetos.

Utilizando ORM o desenvolvedor não precisa se preocupar em escrever a SQL diretamente, a ferramenta ORM (no meu caso o Data Mapper) representa as tabelas do banco de dados com classes, e instâncias destas classes fazem as manipulações e consultas necessárias.

As principais vantagens do ORM é a produtividade, pois o código escrito reduz bastante, facilitando desenvolver e manter aplicações.

Eu comecei recentemente a utilizar o Code Igniter (o primeiro projeto é o novo And After) e estou gostando muito, estou muito mais produtivo mesmo trabalhando com linguagem e ferramentas que ainda não domino completamente: PHP, Code Igniter e ORM.

 

Database

Database,database… qual era aquela coluna mesmo?

Data Mapper

Data Mapper (DM) é um ORM para quem utiliza o framework Code Igniter (CI), e facilita muito qualquer consulta e manipulação ao banco.

O primeiro passo é instalar o Data Mapper no seu CI, seguindo os passos abaixo:

  1. Baixe a última versão do DM e descompacte
  2. Opcional: faça suas configurações do DM no arquivo application/config/datamapper.php
  3. Coloque o  application/config/datamapper.php na pasta application/config folder do seu CodeIgniter
  4. Coloque o application/libraries/datamapper.php na pasta application/libraries do seu Code Igniter.
  5. Coloque o application/languages/english/datamapper_lang.php na pasta application/language/english do seu Code Igniter (aqui você pode adicionar os idiomas que quiser, eles contem as mensagens de erro do DM).
  6. Configue o  application/config/autoload.php e adicione a biblioteca do datamapper e database no array do autoload

$autoload["libraries"] = array("database", "datamapper");

Com isso seu aplicativo deve estar pronto para utilizar o Data Mapper!

Abaixo vou publicar exemplos básicos de uso do Data Mapper para consultar o seu banco alterar e inserir dados. Será bem superficial, o objetivo deste post é introduzir  ORM e o Data Mapper para quem está começando ou pretende começar a utilizar o Code Igniter.

 

Começando – uma classe para cada tabela

Relembrando: utilizando uma ferramenta ORM (neste caso o Data Mapper) cada tabela será representada por uma classe no seu Model (usando MVC), e esta classe extenderá a classe do Data Mapper.

Como exemplo vou usar parte do código do backend desenvolvido para o And After, temos uma tabela users, então na camada Model temos a seguinte classe:

class User extends DataMapper {
    var $table = "users";

    function User()
    {
        parent::DataMapper();
    }
}

 

Neste exemplo acima a definição da variável $table é opcional, pois quando ela não está definida o Data Mapper automaticamente define table como o plural (adicionando o "s" ao final) do nome da sua classe. 

 

Consultando o banco de dados – Função Get

Agora vamos ver como o Data Mapper aumenta a produtividade no desenvolvimento, no exemplo abaixo vou fazer uma consulta que retorne todas as linhas e colunas da tabela users, utilizando a classe User criada no exemplo acima.

$u = new User();
$u->get();

Fácil assim, o $u é um objeto com todos os resultados da nossa consulta que seria o mesmo que:

get * from users

 

Limit

Sabemos que não é bom para a performance da nossa aplicação o get * e nem consultas sem um LIMIT definido, para definir o limite é simples, vamos retornar 5 usuários apenas:

$u = new User();
$u->get(5);

E no exemplo abaixo, pegando 5 usuários começando a partir do 10:

$u = new User();
$u->get(5,10);

 

Selecionando as colunas – select

E agora selecionando as colunas que serão retornadas para evitar consultar dados desnecessários, vamos pegar apenas o user e email dos usuários.

$u = new User();
$u->select("user,email");
$u->get(5);

 

Fazendo buscas – where

Vamos buscar o usuário que tenha o user "tester":

$u = new User();
$u->where("user", "tester");
$u->get(1);

 

Você pode usar diversos "where" em uma mesma consulta, e existe também o "or_where" como no exemplo abaixo:

Fazendo buscas – or_where

$u = new User();
$u->where("user", "tester");
$u->or_where("id", "1");
$u->get();

Neste caso vai retornar 1 user se o user com ID 1 for o "tester" ou dois, caso eles sejam diferentes.

 

Ordenando a consulta – order_by

Pegando 15 usuários ordenados pelo nome:

$u = new User();
$u->order_by("name asc")
$u->get(15);

 

Aqui estão os exemplos básicos de consultas utilizando o Data Mapper, recomendo que você leia e releia a documentação da função Get (en) para consultas avançadas.

 

Manipulando os objetos da consulta

Agora você aprendeu a fazer a consulta, já tem sua camada de Model estruturada com uma classe para cada tabela, vamos manipular os dados!

Para exemplificar como é a manipulação dos dados de uma consulta, vamos criar um array com o nome dos últimos usuários cadastrados:

//Consulta os usuários ordenado pela data
$u = new User();
$u->select("name");
$u->order_by("date DESC");
$u->get(10);

//Vamos criar um array com o nome dos usuários da consulta acima
$arr = array();
foreach ($u->all as $user)
{
    $arr[] = $user->name;
}

 

Quando o resultado é apenas um objeto pode ser feito da seguinte forma:

$u = new User();
$u->select("id,name");

$u->get(1);

echo $u->id ." - ". $u->name;

 

Espero que você, que usa o Code Igniter mas ainda não usa o Data Mapper em breve seja mais um adepto!

O próximo post sobre o Code Igniter será o relato da dificuldade que tive para estruturar a primeira aplicação e dividir corretamente as camadas Model, View e Controller (MVC) e do Data Mapper já tenho um começado que trata de relacionamento de tabelas do banco de dados utilizando esta ferramenta.

Espero que gostem, dúvidas, críticas e sugestões nos comentários!