DataMapper: Object Relational Mapper en CodeIgniter

PiradoIV

En esta ocasión vamos a acelerar el desarrollo de nuestras aplicaciones, gracias a la técnica Object Relational Mapper. Para ello, vamos a utilizar una biblioteca bastante madura, sencilla y muy bien documentada, llamada DataMapper.

Instalación y configuración

¿Qué vamos a necesitar?

1.- CodeIgniter (Este tutorial trata sobre la versión 1.7.3)
2.- DataMapper (1.8.0 es la más actual, en el momento de escribir este tutorial)

La instalación de CodeIgniter es muy sencilla, como ya vimos en el anterior tutorial. Para añadir DataMapper al proyecto, simplemente tenemos que copiar el contenido del directorio "application" dentro del directorio "system/application" de nuestro CodeIgniter.

Configuración

Si todo ha ido bien, deberíamos poder configurar DataMapper editando el fichero "system/application/config/datamapper.php". En general, aunque dependiendo de la estructura de tu base de datos, para lo único que necesitas editar este fichero es para añadir las extensiones que queramos utilizar, ya hablaremos más adelante de esto. En cualquier caso, si tienes dudas, la documentación ofrece una tabla con la descripción de todos los parámetros de configuración.

Estructura de la base de datos

En este ejemplo vamos a dar los primeros pasos para crear un juego de rol, así que los modelos que vamos a necesitar en la base de datos serían:

· Usuarios ( users: id, username, password, created, updated )
· Personajes ( players: id, name, life, created, updated )
· Clases de personajes ( characters: id, name, created, updated )
· Objetos que llevan los personajes ( items: id, name, quantity, created, updated )

Aunque se puede configurar de otra manera a la hora de preparar el modelo de CodeIgniter, por defecto debemos escribir el nombre de las tablas de la base de datos en el plural del inglés. Básicamente hay que añadirle una s al final de la palabra, salvo las excepciones en las que terminan en y. Por ejemplo para el modelo pony, el nombre de la tabla sería ponies.

Tablas intermedias para las relaciones

DataMapper permite relaciones del tipo One to One, One to Many y Many to Many.

En nuestro juego, cada usuario puede tener un personaje (One to One), a su vez, un personaje sólo puede ser de un tipo (One to One). Sin embargo, un personaje puede llevar muchos objetos (One to Many).

Todas estas relaciones necesitan sus tablas en la base de datos, con esta estructura:

· players_users: id, player_id, user_id, created, updated
· characters_players: id, character_id, player_id, created, updated
· items_players: id, item_id, player_id, created, updated

El nombre de la tabla sería, en orden alfabético y plural, los dos nombres de las tablas que queremos relacionar.

Preparando los modelos en CodeIgniter

Ahora es cuando viene lo bueno de verdad, vamos a preparar los siguientes modelos en system/application/models:

User.php

<?php

class User extends DataMapper {

var $has_one = array('Player');

}

Un usuario nada más que puede tener un personaje.

Player.php

<?php

class Player extends DataMapper {

var $has_one = array('User', 'Character');
var $has_many = array('Item');

}

Un personaje puede tener un único usuario y una única clase, pero puede tener muchos objetos.

Character.php

<?php

class Character extends DataMapper {

var $has_many = array('Player');

}

Una clase de personaje puede pertenecer a su vez a muchos personajes, esto nos sirve para listar todos los personajes de una clase concreta.

Item.php

<?php

class Item extends DataMapper {

var $has_one = array('Player');

}

En nuestro juego, los objetos son físicos y no pueden pertenecer a varios personajes al mismo tiempo, si quisiéramos extender el juego, lo suyo sería hacer también clases de objetos.

Te lo creas o no, esa es la configuración más básica de nuestros modelos de objeto, del resto se encarga DataMapper... ¿cuánto tiempo has ahorrado preparando los modelos? =)

Cosas a destacar:

  • Los modelos extienden a DataMapper, en lugar de a Model.
  • Si queremos especificarle el nombre de la tabla -> var $table = 'tabla';
  • Puedes consultar la documentación sobre la creación de modelos para ampliar información.

Controladores

Vamos a poner en acción esos modelos que hemos preparado para que veas el potencial. ¿Qué tal si creamos a nuestro primer usuario?:

// Lo primero es cargar nuestro modelo y
// crear una instancia del mismo
$this->load->model('User');
$user = new $this->User();

// Directamente le asignamos las variables
$user->username = 'piradoiv';
$user->password = md5('1234');

// Y lo guardamos en la base de datos
$user->save();

DataMapper se encargará de establecer automáticamente los campos created y updated.

Vamos a practicar un poco más creando un par de tipos de personajes:

$this->load->model('Character');
$barbarian = new $this->Character();
$assassin = new $this->Character();

$barbarian->name = 'Bárbaro';
$assassin->name = 'Asesina';

$barbarian->save();
$assassin->save();

Ahora vamos a preparar las relaciones, con un jugador:

// Voy a suponer que estamos en otro
// controlador diferente, así que necesitamos
// volver a cargar los modelos
$this->load->model('User');
$this->load->model('Player');
$this->load->model('Character');

// Instanciamos los modelos
$user = new User();
$player = new Player();
$character = new Character();

// Usamos un poco de magia para
// recuperar el usuario que habíamos
// creado y a mi personaje favorito
// del Diablo II, la asesina :P
$user->get_by_username('piradoiv');
$character->get_by_name('assassin');

// Ahora se lo vamos a asignar
// a un personaje nuevo
$player->name = 'Pirado IV';
$player->save();

// Para guardar relaciones, basta con
// invocar el método save, pasándole
// como parámetros los objetos
// relacionados.
$player->save(array($user, $character));

Como puedes ver, podemos hacer búsquedas con get_by_field. Otra manera de hacer exactamente lo mismo sería:

$user->get('username', 'piradoiv');

// Aprovechamos para actualizarle la
// contraseña al usuario
$user->password = md5('hoygan');

$user->save();

Como ves, save() también actualiza la base de datos.

Generando listados

Otra de las típicas cosas que vamos a querer hacer, es generar listados de objetos, vamos a aprovechar la base de datos tal y como la tenemos para practicar:

$characters = new Character();
$characters->get();

foreach($characters->all as $character) {
    echo "$character->name<br />";
}

Si estás familiarizado con ActiveRecord, te sentirás a gusto filtrando y ordenando los listados:

// Ordenar por ID descendiente
$characters->order_by('id', 'desc')->get();

// O buscar con LIKE y limitar a un resultado
$characters->like('character', 'assa')->limit(1)->get();

Tienes todos los ejemplos en la documentación sobre el método Get

Accediendo a las relaciones

¡Casi se me olvida ponerlo!, para acceder a las relaciones basta con ejecutar un get(); en la propiedad a la que quieras acceder. Vamos a verlo con un ejemplo:

// Cargamos el modelo del usuario,
// los demás no nos van a hacer falta
// porque vienen incluídos al acceder
// a la relaciones.
$this->load->model('User');

$user = new $this->User();
$user->get_by_username('piradoiv');

// Cargamos nuestro personaje...
$user->player->get();

// Eso nos descargará únicamente los
// jugadores que tengan relación con
// ese usuario, vamos a descargarnos
// todos los items que tiene este
// usuario
$items = $user->player->item;
$items->get();

foreach($items->all as $item) {

echo "$item->name<br />";

}

Dos cositas más

DataMapper ha sido programado de manera que se le pueden crear extensiones de una manera sencilla. Entre otras, la versión actual incluye:

· Una extensión para generar arrays de tus objetos:

$character->get_by_name('barbarian');
print_r($character->to_array());

· Importar y exportar objetos desde ficheros CSV

$characters->get();
$characters->csv_export('/fichero.csv');

Tiene unas cuantas más, lo mejor es que le eches un ojo a la documentación. Lo que tienes que tener en cuenta es que para utilizar estas extensiones, debes añadirlas al fichero de configuración (system/application/config/datamapper.php):

$config['extensions'] = array('array', 'json', 'csv');

La otra cosa interesante de DataMapper es que funciona nativamente con las funciones de acceso a la base de datos de CodeIgniter, por lo que podremos utilizar el Profiler para encontrar cuellos de botella en nuestra aplicación. Si encontramos algo que necesitemos pulir, podemos optimizarlo al detalle con consultas directas ActiveRecord, SQL, ...

¡Cambio y corto!

Espero que este tutorial te haya servido de ayuda, ahora eres tú el que debes echarle un ojo en profundidad a la documentación.

http://datamapper.wanwizard.eu/

Y si aun así necesitas una mano, siempre estamos por #mv.nerd en Quakenet y #mvnerds en Twitter.

8
dagavi

¿Estás obligado a generar un campo ID o podrías utilizar directamente campos como "username"?

2 respuestas
Pyronhell

Gracias por el post, en cuanto tenga tiempo lo miraré en profundidad.

BLZKZ

juro que el dia 9 me lo voy a leer xD ahora estoy de examenes :( Pero vamos que me viene genial

PiradoIV

Se me había olvidado poner lo más importante, acceder a las relaciones xDDD, ya está actualizado =)

#2 No es obligatorio, no caigo en por qué querrías hacerlo sin ID, pero aquí tienes cómo hacerlo:
http://datamapper.wanwizard.eu/pages/save.html#Save.Existing.As.New

Mejor dicho, sí es obligatorio a no ser que uses el método que te describen arriba.

1
dr_Rouman

Cómo te lo curras :D

Es muy muy parecido a como maneja django la BD

1 respuesta
PiradoIV

#6 sí, django usa ORM también, lo bueno es que una vez que aprendamos a manejarnos con estas técnicas, nos va a dar igual en qué lenguaje programemos, que siempre haremos más o menos lo mismo =)

NeB1

#2 Acostumbrate a usar ID's numéricas xD

DarkSoldier

#1 +1 XD

10 días después
liebgott

Estoy empezando con Codeigniter y DataMapper y tengo una pregunta muy fundamental que no he podido responder por mí mismo buscando en la documentación.

Tengo una base de datos muy simple con tres tablas:

estudiante
cursos
student_courses

Entiendo cómo funcionan las relaciones, hacer operaciones CRUD con IC, etc .... pero ¿Cómo puedo establecer los relaciones entre los estudiantes y los cursos mediante un formulario que debe llenar un usuario?

Imagínaos que tengo un formulario en el que un usuario tiene que rellenar el nombre de un estudiante y seleccionar dos o más cursos ... como se podria hacer?

Muchas gracias

2 meses después
BLZKZ

entonces las relaciones se hacen en tablas externas... si quiero hacer esto tengo que mandar a tomar por culo a las fk que tenia metidas en las tablas -.-" mas trabajo cagonros

#1 haciendo lo que dices me salta este bonito error (mirando por los foros de CI no saco mucho en claro)

tablas:

users: id, name, nick, password, email, birth, avatar, n_post, last_con, created, updated
threads: id, title, replay, created, updated

threads_users: id, thread_id, user_id, created, updated

User.php:

<?php
class User extends DataMapper {
	var $has_many = array ('Thread', 'Post');
}

Thread.php:

<?php
class Thread extends DataMapper {
	var $has_one = array ('User', 'Forum');
	var $has_many = array ('Post');
}

En fin xD

Donde me da el error es aqui:

		$this->load->model('User');
		$this->load->model('Forum');
		$this->load->model('Thread');
		$this->load->model('Post');
		
	$user = new User();
	$forum = new Forum();
	$thread = new Thread();
	$post = new Post();
	
	$user->get_by_nick('blzkz');
	$forum->get_by_id(1);
	$thread->get_by_id(1);
	$post->get_by_id(1);
	
	//$thread->save($post->all);
	$thread->save($user);

Tengo más cosas debajo que nunca llegan a ejecutarse porque me da error antes -.-

1 respuesta
PiradoIV

#11 Ahí te paso la estructura de la base de datos, los modelos y el controlador, a modo de guía, usando CodeIgniter 2.0.2 y DataMapper 1.8.0:

Base de datos:

CREATE TABLE `threads` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `title` (`title`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

# ------------------------------------------------------------

CREATE TABLE `threads_users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `thread_id` int(11) DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `thread_user` (`thread_id`,`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

# ------------------------------------------------------------

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `nick` varchar(18) DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `nick` (`nick`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Modelo User:

<?php

class User extends DataMapper {

var $has_one = array();
var $has_many = array('thread');

function __construct($id = NULL) {
	parent::__construct($id);
}

}

/* End of file User.php */
/* Location: ./application/models/User.php */

Modelo Thread:

<?php

class Thread extends DataMapper {

var $has_one = array('user');
var $has_many = array();

function __construct($id = NULL) {
	parent::__construct($id);
}

}

/* End of file Thread.php */
/* Location: ./application/models/Thread.php */

Controlador:

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Welcome extends CI_Controller {

public function index() {
	$this->load->view('welcome_message');
}

public function test() {
	
	$this->load->model('User');
	$this->load->model('Thread');
	
	$user = new $this->User();
	$thread = new $this->Thread();
	
	$user->nick = 'blzkz';
	$thread->title = 'Probandoooo';
	
	$user->save();
	$thread->save($user);
	
}
}

/* End of file welcome.php */
/* Location: ./application/controllers/welcome.php */

¡Saludos!

BLZKZ

gracias por la ayuda, era el $this al crearlo que al no ponerlo no instanciaba al objeto que debia ffs php y ffs ci xD

Solved!

2 meses después
xCoNDoR

#1 Gracias a ti he descubierto esto, para entenderlo mejor he intentado seguir tu mini guía, pero me urge un problema..

Estoy en un controlador, haciendo las relaciones con un jugador:

public function relaciones()
		{
			$this->load->model('User');
			$this->load->model('Player');
			$this->load->model('Character');

$user = new $this->User();
$player = new $this->Player();
$character = new $this->Character();

$user->get_by_username('condor');
$character->get_by_name('Barbaro');
		
$player->name = 'Condor III';
$player->save();

$player->save(array($user, $character));
	
	}

El error es, Unable to relate player with user... Y no tengo ni idea a que se debe.. te agradecería un poco de tu conocimiento

EDIT:

Puede ser por no tener definido campos 'KEY' en las bases de datos ? No me entero muy bien de eso, está en algun lado explicado ?

PiradoIV

Tienes que crear estas tablas en la base de datos:

  • characters: id, name, ...
  • characters_players: id, character_id, player_id
  • players: id, name, ...
  • players_users: id, player_id, user_id
  • users: id, username, ...

También tendrás que crear los modelos:

  • character: $has_one = array('player');
  • player: $has_one = array('character', 'user');
  • user: $has_one = array('player');

Y listo, debería funcionarte con eso.

C

Qué bonitos son los ORM cuando se trata de dar de alta un usuario... Cuando hablamos de una base de datos de un ERP en constante crecimiento con cientos de tablas y con decenas de particularidades en función de cada cliente y/o instalación...

No es una crítica, en absoluto. Es una reafirmación en que cada herramienta es para lo que es. Supongo que para montar un MV es perfecto un ORM. Un esquema con un crecimiento controlado y con un solo escenario de funcionamiento. Pero para otro tipo de software es bastante inoperativo. Me refiero a cuando hay que retocar ad-hoc ciertos módulos de un software en función de parámetros externos. A cuando hay que parchear y "trucar" el enlace entre tablas, por ejemplo.

Se me olvidaba, gracias #1 por el tutorial de iniciación tan claro y limpio. Así da gusto entrar a este foro.

1 1 respuesta
Tig

#16 algo parecido me ha pasado hace poco con Symfony2 + Doctrine2. Para persistir unos cuantos objetos muy bien, pero en un paso que tenía que persistir unos 1000 de golpe, allowed memory size error al canto.

En cualquier caso, para mi proyecto sigue siendo válido un ORM, simplemente es importante hacer flushes y cleans por cachos y no esperar a tener todo para volcarlo a la base de datos, cuando con mysql puro y duro esto no suele causar problemas.

También hay que comentar lo cómodo que es trabajar con objetos sin tener que preocuparte de lo que hay por detrás.

eisenfaust

En teoría un ORM es el camino a seguir si quieres encapsular realmente un producto, sobre todo ahora con la moda del NoSQL y el gran número de opciones, pero a nada que vayas a un gran cliente (un banco, por ejemplo) y veas sus PLs de 300 líneas toqueteadas durante años por decenas de empleados sin mucha idea... caja negra y a otra cosa.

De momento la idea está ahí, pero no da para mucho cuando hay código de legado de por medio o la eficiencia sea un problema.

24 días después
kolka

Tengo un problema que no logro solucionar y he buscado respecto al tema pero no he encontrado nada:

Digamos que tenemos un producto X con unas características. Por otro lado tenemos una serie de tiendas. Este producto puede ser vendido por N tiendas a un precio distinto en cada una. Siendo una relación N:M, ¿como se guarda en el datamapper el precio? Se debería de almacenar en la relación, pero no veo el como.

1 respuesta
MTX_Anubis

#19 sin tener ni idea de datamapper, eso es una relación many to many con atributo, en este caso el precio.

oh mira:

http://datamapper.wanwizard.eu/pages/joinfields.html

1 respuesta
kolka

#20 Mira que me he mirado de arriba abajo la documentación y me he saltado eso...

gracias :)

1 mes después
yusukorz

Tengo una duda, por lo que entiendo del MVC, el Modelo es la parte que interactua con la base de datos, sin embargo, veo (no solo aquí, sino en la documentación del DataMapper) que en el Controlador se hacen llamadas a la base de datos, ya sea mostrar, guardar etc y el Modelo queda limpio, solo con la configuración de las relaciones, porque ?

3 respuestas
BLZKZ

#22 no no, solo hay dos partes la vista y el controlador, modelo es de "modelizar" un sistema de trabajo por ejemplo. En la vista lo unico que haces es mostrar datos (output streams), en el controlador haces todos los calculos y llamadas que necesites.

Confundes conceptos asi que ahora aplica lo que te he dicho y trata de explicarlo mejor (y pon los ejemplos que dices) y así te ayudaremos seguro :)

EDIT: vale acabo de caer con lo que te refieres a modelo, y es que tienes un cacao impresionante.
Los modelos del ORM (datamapper) nada tiene que ver con el MVC de manera directa, el modelo vista controlador es un patrón que consta de dos partes, el controlador y la vista.

Los modelos que defines usando el ORM es para definir las relaciones entre los objetos (que en este caso representan las tablas) y eso simplemente es una herramienta que te ayuda a manejar las bases de datos desde los controladores (te deja definir objetos de clase modelo y usarlos como si realmente los tuvieras y te ahorras hacer a mano las consultas SQL). A las vistas le pasas todo lo que necesitas mostrar y tal, simple y llanamente xD

xCoNDoR

Igual yo he entendido a #22 de otra forma, pero creo que la respuesta para ti es:

En el controlador puedes hacer llamadas a bases de datos también, en verdad puedes hacer lo mismo que desde un modelo, simplemente se utilizan los modelos para hacer todo mas limpio y ordenado, si tienes un modelo que gestiona las consultas (en ocasiones largas y grandes) de tu base de datos, tu controlador que es el que se encarga de pedirle consultas a los modelos y aplicarselas a las vistas, pues queda mucho mas limpio. Si algunas consultas son rapidas, pues no hace falta llamar a un modelo y se hace directamente en el controlador, en verdad es como tu quieras organizarte.

1 respuesta
yusukorz

#24 A eso me refería, estoy acostumbrado a eso, en el Modelo meter toda gestión de datos y desde el Controlador hacer llamadas al Modelo, por eso preguntaba si por ser CodeIgniter o el ORM la cosa va diferente.

PiradoIV

#22 Sí, puedes hacerlo en el modelo sin problemas, es lo suyo =)

yusukorz

Ok! Gracias a los tres por contestar.

Usuarios habituales

  • yusukorz
  • PiradoIV
  • xCoNDoR
  • BLZKZ
  • kolka
  • eisenfaust
  • NeB1