miércoles 16 de mayo de 2007

Patron ActiveTable y ActiveRecord

Patrón ActiveTable y ActiveRecord

Cuando empezamos a utilizar OOP al 100% empezamos a usar clases para representar objetos que estamos utilizando en el diseño de nuestro sistema.

Pensemos en un ejemplo básico, vamos a hacer un catalogo de automóviles, para representar a nuestro automóvil vamos a utilizar la siguiente clase:



1  <?php
2  
class Automovil {
3       private 
$nombre;
4       private 
$color;
5       private 
$puertas;
6       private 
$maxVelocidad;
7       
8       public function 
__construct() {}
9       
10       
/** Setters **/
11       
public function setNombre$nombre ) {
12            
$this->nombre $nombre;
13       }
14       
15       public function 
setColor$color ) {
16            
$this->color $color;
17       }
18       
19       public function 
setPuertas$puertas ) {
20            
$this->puertas $puertas;
21       }
22       
23       public function 
setMaxVelocidad$velocidad ) {
24            
$this->maxVelocidad $velocidad;
25       }
26       
27       
/** Getters **/
28       
public function getNombre() {
29            return 
$this->nombre;
30       }
31       
32       public function 
getColor() {
33            return 
$this->color;
34       }
35       
36       public function 
getPuertas() {
37            return 
$this->puertas;
38       }
39       
40       public function 
getMaxVelocidad() {
41            return 
$this->maxVelocidad;
42       }
43  }
44  
?>



Como podemos ver nuestro automóvil tiene propiedades básicas, puede ser más complejo pero para este tutorial lo dejaremos así. Podemos utilizar nuestro nuevo objeto Automóvil de la siguiente manera:



1  <?php
2  $auto 
= new Automovil();
3  
$auto->setNombre("BMW M3");
4  
$auto->setColor("Negro");
5  
$auto->setPuertas);
6  
$auto->setMaxVelocidad"250 km/h" );
7  
?>



Es muy sencillo, pero se complica cuando queremos que este automóvil sea persistente, o sea que este guardado en un medio no volátil, como una base de datos, XML, archivo de texto, etc. Esto es fácil de implementar, por ejemplo usando serialize y guardándolo en esos medios, pero ¿cómo lo recuperamos?, ¿Cómo podemos hacer búsqueda por cierta característica (ej: el color)? Aquí es donde se complica todo, ya que si queremos tener una tabla en nuestra base de datos donde cada propiedad de nuestra clase represente una columna de nuestra base de datos, y cada instancia de la clase represente una fila de nuestra tabla necesitamos de cierta forma hacer que se traduzca la clase en un objeto que se pueda insertar en nuestra base de datos.

La forma de hacer esto: ActiveRecord, este es un patrón de diseño, con el cual podemos implementar de manera correcta que cada objeto nos represente una fila de nuestra base de datos.

¿Qué es el ActiveTable?

El Active Table es un objeto que puede persistir otros objetos en medios no volátiles, como bases de datos. La función de una clase que implementa este patrón es de comunicarse con la base de datos y regresarnos objetos como los tenemos definidos para nuestra aplicación. Veamos el siguiente ejemplo:



1  <?php
2  $autos 
= new Automoviles();
3  
$unAuto $autos->findByPk(1); // Recuperamos el Auto con ID=1
4  
echo $unAuto['Nombre'];
5  
$unAuto['Puertas'] = 3// Cambiamos el numero de puertas
6  
$unAuto->save(); // Guardamos la modificacion.
7  
?>



Como podemos ver, nuestro ActiveTable Automóviles nos trae un objeto automóvil, sobre el cual podemos trabajar e incluso guardar directamente en la base de datos, el objeto es lo suficientemente inteligente para saber guardarse o crear uno nuevo.

¿Cómo implementamos este patrón?

Hay varias formas de implementarlo, muchos Frameworks lo hacen a su manera, yo en lo personal me gusta la siguiente forma, en la que heredando la clase directamente, podemos saber el nombre de la tabla y usamos comandos simples para obtener los datos que nos interesan (como la clave primaria).

Una vez creado nuestro objeto, a la hora de pedirle que nos regrese algo (con alguna de las funciones find*), la clase debe de crear y regresarnos un objeto que podamos utilizar (en nuestro caso un objeto Automóvil). Este objeto nos sirve para trabajar como lo vimos en el primer ejemplo, pero también a la hora de implementar ActiveRecord es posible que el objeto se sepa guardar en la base de datos solos.

Veamos el siguiente ejemplo de ActiveTable:



1  <?php
2  
class ActiveTable {
3       protected 
$table;
4       protected 
$keyField;
5       protected 
$_db;
6       protected 
$resultObject;
7       private 
$_columns;
8       
9       public function 
__construct() {
10            
$this->_db     DB::getInstance();
11            
$this->setup();
12       }
13       
14       public function 
__destruct() {}
15       
16       protected function 
setup() {
17            if( !
$this->table $this->table get_class($this);
18            
19            
$query "SHOW FIELDS FROM ?";
20            
$command $this->_db->prepare$query );
21            
$command->execute( array( $this->table ) );
22            
23            
$fields = array();
24            
$primary '';
25            while( 
$row $command->fetch() ) {
26                 
$fields[$field['Field']] = array(
27                      
"name" => $field['Field'],
28                      
"type" => $field['Type'],
29                      
"defaultValue" => $field['Default'],
30                      
"key" => $field['Key'],
31                 );
32                 
33                 if( 
$field['Key'] === "PRI" ) {
34                      
$primary $field['Field'];
35                 }
36            }
37            
38            
$this->_columns $fields;
39            
40            if( empty( 
$primary ) ) {
41                 throw new 
Exception"Primary Column not found for Table: " $this->table );
42            }
43            
44            
$this->keyField $primary;
45       }
46       
47       public function 
info() {
48            return array(
49                 
"name" => $this->table,
50                 
"columns" => $this->_columns,
51                 
"primary" => $this->keyField,
52            );
53       }
54       
55       public function 
getPrimaryKey() {
56            return 
$this->keyField;
57       }
58       
59       public function 
newRecord() {
60            
$obj $this->resultObject;
61            
62            
$aObj = new $obj(null$this);
63            return 
$aObj;
64       }
65       
66       public function 
findByPk$id ) {
67            
$sql "SELECT * FROM `%s` WHERE `%s`='%s' LIMIT 1";
68            
$sql sprintf($sql$this->table$this->keyField$id);
69            
70            
$result $this->_db->query$sql );
71            
$data $result->fetch();
72            
73            
$obj $this->resultObject;
74            
75            
$aObj = new $obj($data$this);
76            
77            return 
$aObj;
78       }
79       
80       public function 
add$fields$function_fields = array() ) {
81            
$qvalues = array();
82            
$qfields = array();
83            foreach( 
$fields as $field_name => $field_value ) {
84                 
$qfields[] = "`$field_name`";
85                 if( !empty( 
$function_fields[$field_name] ) ) {
86                      
$qvalues[] = $function_fields[$field_name] . "('$field_value')";
87                 } else {
88                      
$qvalues[] = "'" addslashes$field_value ) . "'";
89                 }
90            }
91            
92            
$query "INSERT INTO `" $this->table "` (" implode', '$qfields ) . ") VALUES ( " implode', '$qvalues ) . ")";
93            
94            
$affRows $this->_db->exec$query );
95            
$id $this->_db->lastInsertId();
96            
97            if( 
$affRows <= ) {
98                 throw new 
Exception"SQL Error, Insert Failed" );
99            }
100            
101            return 
$id;
102       }
103       
104       public function 
save$fields$strict true$function_fields = array() ) {
105            if( ( isset( 
$fields[$this->keyField] ) ) && ( !empty( $fields[$this->keyField] ) ) && $this->keyExists$fields[$this->keyField] ) ) {
106                 
$result $this->update$fields$strict$function_fields );
107            } else {
108                 
$result $this->add$fields$function_fields );
109            }
110            
111            return 
$result;
112       }
113       
114       public function 
update$fields$strict true$function_fields = array() ) {
115            if( !isset( 
$fields[$this->keyField] ) || empty( $fields[$this->keyField] ) ) {
116                 throw new 
Exception"Primary Key Missing, update failed" );
117            }
118            
119            
$key $fields[$this->keyField];
120            unset( 
$fields[$this->keyField] );
121            
122            if( 
$strict ) {
123                 
$rst $this->_db->querysprintf"SELECT * FROM `%s` WHERE `%s`='%s' LIMIT 1"$this->table$this->keyField$key ) );
124                 
$data $rst->fetch();
125            } else {
126                 
$data = array();
127            }
128            
129            
$toupdate = array();
130            foreach( 
$fields as $field=>$value ) {
131                 if( 
$strict ) {
132                      if( ( 
$data[$field] != $value ) ) {
133                           if( !empty( 
$function_fields[$field] ) ) {
134                                
$toupdate[] = sprintf"`%s`=%s('%s')"$field$function_fields[$field], $value );
135                           } else {
136                                
$toupdate[] = sprintf"`%s`='%s'"$fieldaddslashes$value ) );
137                           }
138                      }
139                 } else {
140                      if( !empty( 
$function_fields[$field] ) ) {
141                           
$toupdate[] = sprintf"`%s`=%s('%s')"$field$function_fields[$field], $value );
142                      } else {
143                           
$toupdate[] = sprintf"`%s`='%s'"$fieldaddslashes$value ) );
144                      }
145                 }
146            }
147            if( 
count$toupdate ) == ) { 
148                 return 
0;
149            }
150            
151            
$query sprintf"UPDATE `%s` SET %s WHERE `%s`='%s' LIMIT 1"$this->tableimplode', '$toupdate  ), $this->keyField$key );
152            
153            
$updated $this->_db->exec$query );
154            
155            if( 
$updated <= ) { // if true, then some wierd database error ocurred
156                 
return 0;
157            }
158            
159            return 
$updated;
160       }
161       
162       public function 
delete$records ) {
163            if( empty( 
$records ) ) return 0;
164            
165            if( 
is_array$records ) ) { 
166                 
$escaped_keys = array();
167                 
168                 foreach( 
$records as $key ) {
169                      
$escaped_keys[] = "'{$key}'";
170                 }
171                 
$keys implode(","$escaped_keys);
172                 
$total count$escaped_keys );
173                 
174                 
$query sprintf"DELETE FROM `%s` WHERE `%s` IN (%s) LIMIT %s"$this->table$this->keyField$keys$total );
175            } else {
176                 
$total 1;
177                 
$query sprintf"DELETE FROM `%s` WHERE `%s`='%s' LIMIT %s"$this->table$this->keyField$records$total );
178            }
179            
180            
$deleted $this->_db->exec$query );
181            
182            return 
$deleted;
183       }
184  }
185  
?>



Este es un ejemplo sencillo desde el cual ustedes pueden seguir y completarlo e implementar ya cosas más complejas.

¿Qué es el ActiveRecord?

El Active Record es un patrón de diseño donde a la hora de implementarlo debe de ser inteligente para saber guardarse en la base de datos con solo llamar a su función save.

¿Cómo implementamos este patrón?

Una forma sencilla y rápida es usando ArrayAccess y heredando directamente de ActiveRecord, una implementación sencilla pero efectiva puede ser la siguiente:



1  <?php
2  
class ActiveRecord implements ArrayAccess {
3       private 
$record = array();
4       private 
$keyField '';
5       private 
$table null;
6       private 
$isNew false;
7       
8       public function 
__construct$data$table ) {
9            if( 
$data === null ) {
10                 
$this->record = array();
11                 
$this->isNew true;
12            } else {
13                 
$this->record $data;
14            }
15            
16            if( !( 
$table instanceof ActiveTable ) ) {
17                 throw new 
Exception'$table is expected to be a GeckoDBTable instance' );
18            }
19            
20            
$this->table $table;
21            
$this->keyField $table->getPrimaryKey();
22       }
23       
24       public function 
refresh() {
25            if( 
$this->isNew ) return; // new Records can't be refreshed
26            
27            
$newData $this->table->findByPk$this->record[$this->keyField] );
28            
$this->record $newData->record;
29            
$newData null;
30       }
31       
32       public function 
save() {
33            if( 
$this->isNew ) {
34                 
$id $this->table->add$this->record );
35                 
$this->record[$this->keyField] = $id;
36                 
$this->isNew false;
37                 
38                 return 
$id;
39            } else {
40                 return 
$this->table->update$this->record );
41            }
42       }
43       
44       public function 
getNew() {
45            return new 
selfnull$this->table );
46       }
47       
48       public function 
offsetExists$offset ) {
49            return (isset( 
$this->record[$offset] ) );
50       }
51       
52       public function 
offsetGet$offset ) {
53            return 
$this->record[$offset];
54       }
55       
56       public function 
offsetSet$offset$value ) {
57            if( 
$offset == $this->keyField ) {
58                 throw new 
Exception"Primary Key can't be set or changed" );
59            }
60            
61            
$this->record[$offset] = $value;
62       }
63       
64       public function 
offsetUnset$offset ) {
65            if( isset( 
$this->record[$offset ) ) {
66                 unset( 
$this->record[$offset] );
67            }
68       }
69  }
70  
?>



Ahora ya teniendo nuestros patrones y nuestras clases listas, ¿cómo lo utilizamos?, veamos el siguiente ejemplo completo:



1  <?php
2  
include( 'ActiveTable.php' );
3  include( 
'ActiveRecord.php' );
4  
5  class 
Automoviles extends ActiveTable {
6       public function 
__construct() {
7            
parent::__construct();
8            
9            
$this->resultObject "Automovil";
10       }
11  }
12  
13  class 
Automovil extends ActiveRecord {}
14  
15  
$autos = new Automoviles();
16  
$unAuto $autos->findByPk(1); // Recuperamos el Auto con ID=1
17  
echo $unAuto['Nombre'];
18  
$unAuto['Puertas'] = 3// Cambiamos el numero de puertas
19  
$unAuto->save(); // Guardamos la modificacion.
20  
?>



Como podemos ver, no usamos 1 sola sentencia SQL directamente, esto nos permite tener un nivel de abstracción muchísimo mayor e inclusive a la hora de programar es mucho más rápido.

De aquí faltan mucho más cosas, como por ejemplo hace unos meses en ForosDelWeb alguien decía que esto no era efectivo porque cuando tenemos claves foráneas, teníamos que crear dos instancias y las clases hacían múltiples consultas a la base de datos, esto es cierto, pero no totalmente, podemos modificar nuestra clase para que sea lo suficientemente inteligente y pueda resolver las claves foráneas, y crear Joins, esto es más complicado y no se va a cubrir en este tutorial, pero es posible al 100%, y de manera eficiente.

Espero que con este tutorial les haya quedado como implementar estos patrones de diseño y puedan realizar sus aplicaciones más limpias.



Aqui les dejo un link para que vean como implementar ArrayAccess y como nos permite accesar a las propiedades de un objeto como si fuera un Arreglo.

17 comentarios:

Andrés Guzmán dijo...

hola, esta muy bueno el articulo, también se me ocurre usar los métodos mágicos __get y __set para modificar o obtener los valores del registro en cuestión.

52 public function __get( $offset ) {
53 return $this->record[$offset];
54 }
55
56 public function __set( $offset, $value ) {
57 if( $offset == $this->keyField ) {
58 throw new Exception( "Primary Key can't be set or changed" );
59 }
60
61 $this->record[$offset] = $value;
62 }

felicitaciones y saludos.

Christopher dijo...

Si, se puede hacer asi, aunque a mi gusto es mas limpio si se usa como un array, claro es algo personal ;-)

Gracias por leer mi Blog.

Andrés Guzmán dijo...

Excelente tu blog, gracias a ti por compartir tus conocimientos y felicitaciones.

Saludos.

Manuel Ramon Almaguer Ochoa dijo...

hola, exelente en verdad el articulo te lo agradesco ...
me ha ayudado a enteder un poquito mas la poo, creo que ya es hora de empezar a utilizar cosas como estas...
disculpa mi ignorancia pero tengo una duda: el uso de `%s` ?? por el contexto en las sentecias sql deben ser los valores que le pasas.. bueno yo lo habia usado con este (?).
y me gustaria hacerte una pequena recomendacion. si puedes en ocasiones usaras algunos comentarios mas en el codigo( solo los necesarios)... creeme que nos ayudarias a enteder un poquito mejor el codigo
Un saludo

jahepi dijo...

Gracias Chris, tenía muchísimas ganas de leer este artículo.

Muchas felicidades ! ;-)

masterjail dijo...

Hola, espero que poco a poco vayas poniendo artículos como este.

De momento, aunque haya poco contenido, aquí hay cosas que me resultan interesantes.

Yo estoy intentando hacer mi propio framework y acabo de descubrir que existe esto de ActiveRecord y ActiveTable que yo estaba intentando implementar a mi manera y con otro nombre (porque ignoraba que existía).

Respecto a esto tengo una duda, ¿cómo harías para recuperar un conjunto de registros en vez de uno? Supongamos que tenemos un método findByFk() en la classe ActiveTable y que funciona correctamente...

¿Harías esto?:
$autos = new Automoviles();
$variosAutos = $autos->findByFk(...);

¿O implementarías alguna classe como ActiveRecord que te ayude a manipular mejor un conjunto de registros?

Un saludo.

Christopher dijo...

masterjail:

En tu clase ActiveTable implementas un metodo que se llame ej:

findAll();
findAllByColor();
etc,

Es lo importante de la POO, que puedes heredar a tu clase principal y asi implementar funciones especificas, cada una de estas funciones si te regresan varios registros, te deben de regresar un conjunto de objetos tipo Automovil en el mejor de los casos.

Saludos.

Luis Miguel dijo...

Hola, podrias explicar brevemente estas lineas??

$obj = $this->resultObject;

$aObj = new $obj($data, $this);


que valor tiene $this->resultObject ?????


Saludos

Luis Miguel dijo...

perdon, no habia visto la ultima parte


Saludos

Javier Seixas dijo...

Antes que nada felicidades Christopher por el artículo. La verdad es que es una introducción realmente útil al tema de los patrones de PHP :-D

Tenía la siguiente duda: Estoy provando con la clase ActiveTable. La he copiado directamente y ahora estoy viendo su funcionamiento paso a paso. Sin embargo me he encontrado con un problema. En la línea 21 que escribe "$command->execute( array( $this->table ) );" me da el siguiente error: Fatal error: Call to a member function execute() on a non-object. Si en la línea 19 ($query = "SHOW FIELDS FROM ?";) cambio el "?" por "Automoviles" ya no me da el error.

Mirando sobre el tema he leído la siguiente nota en php.net: PDO::prepare Problem la cual me da a entender que el script no puede funcionar. Cómo habeis conseguido que os funcione vosotros?

Quizá tiene algo que ver la versión PHP? Yo tengo la 5.1.6

Christopher dijo...

Que tal javier_sexias, te recomendaria que actualizaras a la version mas reciente para PHP, y la razon por la que uso placeholders (?) Es porque es mas sencillo para PDO emular los prepared statements para drivers que no los soportan nativamente.

Saludos.

maborak dijo...

loading..........

Muy buen articulo, pero quita los números de los ejemplos. No es fácil hace un copy & paste.

Maborak.

connection closed.

jpaulzm dijo...

excelente articulo. seria bueno q hagas un framework con ActiveRecord y ActiveTable como kumbia framework mucha gente estaria dispuesta a ayudarte.

jchavez dijo...

Saludos, te cuento llege a tu blog enseñandole a un amigo un poco sobre mvc y me ha gustado es un blog bastante interesate.. espero sigas publicand buen contenido

Saludos

jose luis dijo...

Excelente blog y muy util por cierto, gracias por compartir estos conocimientos que son de mucha utilidad.

consulta:
bueno quisiera que me ayudaras con esto:
Al llamarse la funcion setup() desde el constructor de ActiveTable
se muestra el siguiente error:
Primary Column not found for Tablecreo que se debe a que no encuentra la PrimaryKey de la tabla, pero la tabla a la que se hace referencia en la cunsulta si cuenta con clave primaria.
quisiera saver porq no me retorna la clave.
Saludos!!
y Gracias de antemano!!!

Juan pablo dijo...

Hola, muy bueno el post. Si no es mucho pedir, Podrías agregar la clase db?

Muchas gracias

Juan pablo dijo...

Hola, muy bueno el post. Si no es mucho pedir, Podrías agregar la clase db?

Muchas gracias