Relaciones entre entidades: Parte II

Hola a todos! Como os prometí aquí os traigo la segunda y última parte acerca de las relaciones entre las entidades del modelo de nuestras aplicaciones basadas en Yii Framework.

En este post os contaré cómo tratar dos escenarios con los que es común encontrarse cuando trabajamos con dos entidades relacionadas dentro de nuestro modelo. Si recordáis el ejercicio de la entrada anterior, disponíamos de las clases del modelo País y Ciudad y establecimos que, esta última, tuviese una relación de dependencia con la primero. Los escenarios a los que nos enfrentamos son:

  1. ¿Cómo mostrar el listado de nombres de ciudad para poder elegir una desde la vista de países en lugar del identificador?
  2. ¿Cómo mostrar el listado de ciudades en base a un país seleccionado desde un segundo selector?

Entorno de desarrollo

  • OS X Yosemite (10.10.3) – SSOO
  • MAMP (3.2.1) (PHP 5,6,7, MySQL 5.0.11-dev y Apache 2.2.29) – Lenguaje, Servidor y gestor de bases de datos).
  • Sublime Text (3) – Editor de texto
  • Yii Framework (2.0.4) – Framework de desarrollo PHP
  • Composer (1.0-dev) – Gestor de dependencias para PHP

Dónde nos quedamos?

Como os comentaba, en el ejercicio anterior dejamos el proyecto listo para poder empezar a administrar dos nuevas entidades del modelo: País (Country) y Ciudad (City). Apoyándonos en Gii creamos las clases del modelo, la vista y el controlador y a continuación indicamos como, desde nuestro phpmyadmin podíamos establecer la relación entre las dos tablas para que desde Gii se auto-generase el código sin necesidad de “picarlo” nosotros.

1er escenario: relaciones entre tablas

El primer punto lo solventaremos modificando levemente el código de algunos de nuestros ficheros .php de las vistas (view). Actualmente, cuando accedemos al listado de ciudades (cities) desde la url http://localhost:8888/basic/web/index.php?r=city nos encontramos esto:

yiirelationships17

Como ya dijimos, esta forma de visualizar la información no es intuitiva para el usuario, puesto que cuando quiere consultar una ciudad tiene que recordar el país al que hace referencia el identificador que se muestra en la columna Country ID. Así que, para “facilitarle la vida” al usuario, vamos a mostrar el nombre del país en lugar del identificador.

Para ello abrimos el fichero views/city/index.php que es el encargado de mostrar el GridView donde se muestra la información de la anterior imagen, y aquí vamos a modificar el código para que nuestro GridView sea de la siguiente forma:

relationsshipsII1

No os olvidéis de indicar que utilizamos 2 nuevas clases dentro de este fichero por lo que añadid lo siguiente al inicio:

use yii\helpers\ArrayHelper;
use app\models\Country;

Este es el resultado una vez que recargamos la página:

relationsshipsII2

El siguiente paso es aplicar la misma filosofía en el resto de vistas en las que se muestre el identificador del país (Country ID) en lugar del nombre. Empezamos por views/city/view.php donde el código del DetailView quedará de la siguiente forma:

relationsshipsII3

De manera que cuando accedamos al detalle de una de las ciudades del listado veremos algo como esto:

relationsshipsII4

Sigamos aplicando esta misma filosofía, pero esta vez en el formulario de creación/edición de ciudades. Si desde el listado de ciudades pulsamos sobre Create new (http://localhost:8888/basic/web/index.php?r=city%2Fcreate) vemos que el formulario contiene un campo de texto para introducir el identificador del país:

relationsshipsII5

Bien! Pues vamos a hacer que esto sea un campo de selección de los países que ya están dados de alta en la BBDD. Para ello modificamos el código del fichero views/city/_form.php substituyendo la línea:

<?= $form->field($model, 'country_id')->textInput() ?>

Por la siguiente:

use yii\helpers\ArrayHelper;
use app\models\Country;

<?= $form->field($model, 'country_id')
->dropDownList(
ArrayHelper::map(Country::find()->all(), 'id', 'name'))
?>

Si volvemos a recargar la página debemos ver algo parecido a esto:

relationsshipsII6

Os animo a que apliquéis esta técnica a cualquier otra relación que hayáis creado en vuestro proyecto y la compartáis para que podamos conocer cuál ha sido vuestra solución o si habéis encontrado alguna dificultad con la que podamos ayudaros.

2º escenario: selectores dependientes

Con lo anterior explicado, hemos solventado el primer punto de nuestros dos escenarios mencionados al inicio de esta entrada y a continuación abordaremos el segundo punto. Para ello, vamos a poner en práctica el primer punto añadiendo las propiedades País y Ciudad a la entidad del modelo Usuario (user) que creamos en nuestra primera entrada.

Lo primero, como siempre que queremos añadir nueva información al modelo, es definir la columna en la tabla correspondiente de nuestra BBDD, de manera que con la inclusión de los campos Country y City nuestra estructura de la tabla user debe quedar de la siguiente forma:

relationsshipsII7

Os recuerdo los pasos para crear esta estructura:

  1. Crear las columnas city_id y country_id.
  2. Crear un índice por cada una de estas columnas.
  3. Crear la relación de la tabla user con la tabla city y country a partir de los índices.

Aquí debéis tener en cuenta que, cuando vayamos a crear las relaciones, como ya tenemos datos creados en la tabla de nuestros usuarios, phpmyadmin os puede dar el error:

#1452 – Cannot add or update a child row: a foreign key constraint fails ( …

Por lo tanto, entre el paso 2 y 3 debemos exportarnos los datos de los usuarios, eliminar todos los usuarios de la tabla, ejecutar el paso 3 y por último volver a crear los usuarios, o al menos uno con el que podamos acceder a la administración de la aplicación. Os muestro como deberían quedar las relaciones entre tablas:

relationsshipsII8

Para hacerlo más sencillo y que dispongáis de un usuario rápidamente, os paso la cláusula INSERT que lancé al final de estos pasos en lugar de insertar de nuevo todos los usuarios:

INSERT INTO `user` (`id`, `username`, `auth_key`, `password_hash`, `password_reset_token`, `email`, `status`, `created_at`, `updated_at`, `country_id`, `city_id`) VALUES (1, 'vicmonmena', 'xhukgP1kqKtrTqq8qGda62UXZ-a3KNtZ','$2y$13$epIWyKeMjD0nX20ATjjtZeN7xMWs6Mv4uniWPzWa15D0urTTd0zqa', NULL, 'vicmonmena@gmail.com', 10, 1434215225, 1434215225, 2, 2)

Vamos a comprobar que nuestra web continúa funcionando correctamente a pesar de haber incluido estos cambios en la BBDD. Para ello vamos a crear un usuario (http://localhost:8888/basic/web/index.php?r=user%2Fcreate) y vamos a ver si se ha generado el registro en la BBDD:

relationsshipsII9

Todo parece funcionar correctamente! Ahora vamos a adaptar el formulario de creación/edición de usuarios para que podamos escoger la ciudad y el país. Para ello, como hicimos en el escenario 1 añadimos el siguiente código en el fichero views/user/_form.php:

use yii\helpers\ArrayHelper;
use app\models\City;
use app\models\Country;

<?= $form->field($model, 'country_id')
->dropDownList(
ArrayHelper::map(Country::find()->all(), 'id', 'name'))
?>

<?= $form->field($model, 'city_id')
->dropDownList(
ArrayHelper::map(City::find()->all(), 'id', 'city'))
?>

Y vamos a ver cómo queda la página en nuestro navegador:

relationsshipsII10

Todo parece pintar bien; sin embargo, faltan algunos detalles bastante importantes y en los cuales podéis haber caído ya si habéis seguido las anteriores entradas acerca del desarrollo con Yii.

Empecemos por adaptar el código de nuestras entidades del modelo para trabajar con las relaciones. Para ello, nos apoyaremos en Gii (si no habéis leído aún la entrada Yii: Framework Gii, este es un buen momento). Con esto solo tenemos que copiar y pegar el código que Gii nos propone en nuestras clases: User, City y Contry, y en este mismo orden os indico qué código he añadido yo:

User.php

use app\models\City;
use app\models\Country;

/**
* @inheritdoc
*/
public function rules()
{
return [
...
[['country_id', 'city_id'], 'integer'],
];
}

/**
* @return \yii\db\ActiveQuery
*/
public function getCity()
{
return $this->hasOne(City::className(), ['id' => 'city_id']);
}

/**
* @return \yii\db\ActiveQuery
*/
public function getCountry()
{
return $this->hasOne(Country::className(), ['id' => 'country_id']);
}

City.php

use app\models\User;

/**
* @return \yii\db\ActiveQuery
*/
public function getUsers()
{
return $this->hasMany(User::className(), ['city_id' => 'id']);
}

Country.php

use app\models\User;

/**
* @return \yii\db\ActiveQuery
*/
public function getUsers()
{
return $this->hasMany(User::className(), ['country_id' => 'id']);
}

Y ahora vamos a comprobar que efectivamente si elegimos una ciudad y un país estos quedan registrados para nuestro nuevo usuario, creemos un usuario nuevo!

relationsshipsII11

Y veamos que en la BBDD aparece:

relationsshipsII12

Perfecto! Pero…¿No notáis algo que no cuadra? ¿Un usuario que es de Huelva, ciudad de Canadá? Este es el segundo punto importante, filtrar el listado de ciudades en función del país seleccionado para que la información que se introduzca tenga sentido.

Para que sea más sencillo voy a enumerar los pasos que tenemos que dar para adaptar nuestro formulario para que el comportamiento de los selectores funcione de esta forma:

1. Crear una función en el controlador de ciudades que nos devuelva el listado de ciudades filtrado por identificador de país (country_id). Para esto, he creado una nueva action en la clase controllers/CityController.php con el siguiente código:

/**
* Obtiene un listado de ciudades cuyo contry_id coincida con el argumento.
* El formato del listado será HTML de tipo <option value=ID>NAME</option>
*/
public function actionList($id) {
    $cities = City::find()->where(['country_id' => $id])->all();
    $citiesSize = City::find()->where(['country_id' => $id])->count();
    if ($citiesSize > 0) {
        foreach ($cities as $city) {
            echo "<option value='" . $city->id . "'>" . $city->city . "</option>";
        }
     } else {
        echo "<option>-</option>";
     }
}

2. Modificar el código del primer dropDownList (el de países) del formulario views/user/_form.php para que cargue el listado de ciudades en base al país seleccionado. El código quedaría así:

use yii\helpers\Url;

<?= $form->field($model, 'country_id')
->dropDownList(
ArrayHelper::map(Country::find()->all(), 'id', 'name'),
[
'prompt' => 'Selecciona país',
'onchange' => '$.get("' .
Yii::$app->urlManager->createUrl('city/list') . '", { id:                         $(this).val() }).done(
function(data) {
$("select#user-city_id").html(data);
});'
]
);
?>

Varias observaciones sobre el código anterior:

  • Hemos añadido comportamiento al evento onchange para que cuando se seleccione un país, se invoque la action definida en el controlador: list pasándole como argumento el valor seleccionado: $(this).val(), que en este caso es el id del país.
  • Una vez se ejecuta el método definido en CityController (actionList), el código html generado se asigna al dropDownList que hemos incluído en nuestra vista para cargar el listado de ciudades: select#user-city_id.
  • Al final, la URL que se genera mediante la llamada a:

Yii::$app->urlManager->createUrl(‘city/list’) . ‘”, { id: $(this).val() }

debe ser similar a:

http://localhost:8888/basic/web/index.php?r=city%2Flist&id=2

  • Por último, para poder saber cuál es el id que yii auto-genera para el dropDownList de ciudades, debemos localizarlo en el DOM del formulario (_form). Para ello los navegadores traen herramientas de desarrollo ya integradas que permiten explorar los elementos del HTML de las páginas rápidamente, como se aprecia a continuación:

relationsshipsII13

Y el resultado final es…

Aquí os dejo cómo debería quedar el listado de ciudades cuando seleccionamos un España:

relationsshipsII14

Y cuando escogemos Canadá:

relationsshipsII15

Por último os animo a que pongáis en práctica los conocimientos adquiridos hasta el momento y añadáis a las vistas del listado de usuarios (views/user/index.php) y de detalle de usuario (views/user/view.php) estos campos Ciudad y País para que podamos prescindir de phpmyadmin para comprobar a qué ciudad y país pertenece cada usuario. Os dejo cómo debería quedaros al final:

relationsshipsII16

Y eso es todo por ahora, en cuanto a las relaciones entre entidades del modelo. Como siempre, os dejo el código para que lo descarguéis de este repositorio.

Hasta la próxima!

Acerca de vicmonmena

Just trying to contribute with my code to make this world easier
Esta entrada fue publicada en Desarrollo web, yii y etiquetada , , , , , , . Guarda el enlace permanente.

8 respuestas a Relaciones entre entidades: Parte II

  1. hola ¿cómo puedo hacer para agregar más botones al ActionColumn en el CGridview? Necesito poner un enlace personalizado

  2. hola, no se si es pedir mucho, soy nuevo en el mundo de la programación pero tengo algunos conocimientos con yii1 que para la versión 2 no me sirven para nada, quisiera saber como puedo resolver lo de las relaciones bellongs-to de yii1 en yii2, suponiendo que quiero mostrar en una view un dato de otra tabla, ya tengo las relaciones necesarias, lo de las llaves foráneas y todo lo otro, lo que no encuentro es como llamarla, porque las relaciones en el 2 cambiaron y me dejaron fuera de lo poco que ya dominaba. gracias de antemano y felicitaciones por su blog y su esplendida forma de explicar, creo que voy a pasar mucho tiempo por aquí molestándolo con explicaciones de yii2. saludos

  3. vicmonmena dijo:

    Hola Mauro!

    Si te he entendido bien, lo que necesitas está en el código de la clase Country.php y en el de la clase City.php del proyecto de esta entrada, concretamente al final del fichero. Veamos el código de la primera:

    /**
    * @return \yii\db\ActiveQuery
    */
    public function getCities()
    {
    return $this->hasMany(City::className(), [‘country_id’ => ‘id’]);
    }

    /**
    * @return \yii\db\ActiveQuery
    */
    public function getUsers()
    {
    return $this->hasMany(User::className(), [‘city_id’ => ‘id’]);
    }

    Con este código estableces 2 premisas:

    1.- “un país puede estar asociado a 1 o más usuarios”
    2.- “un país puede estar asociado a 1 o más ciudades”

    Esto en Yii 2 se hace mediante la función “hasMany” (http://www.yiiframework.com/doc-2.0/yii-db-baseactiverecord.html#hasMany()-detail).

    Para luego poder mostrar esta relación, por ejemplo con el caso de las Ciudades (cities) puedes echar un ojo al fichero views/city/index.php del proyecto donde se visualiza el listado de ciudades dadas de altas con al información de cada una mostrada por columnas donde una de ellas es el país (Country) al que pertenece, pero mostrar el nombre del país en lugar del identificador (id) para que la información pueda ser interpretada por el usuario. El código que “coloca” el nombre del país correspondiente a cada id lo tienes en el archivo que te indico y sería así:

    [

    [
    ‘attribute’ => ‘country_id’,
    ‘value’ => function($model) {
    $country = Country::findOne($model->country_id);
    return $country->name;
    },
    ‘filter’ => ArrayHelper::map(Country::find()->all(), ‘id’, ‘name’),
    ],

    ],
    ]); ?>

    Como ves, añadimos una columna que carga la información desde la BBDD (Country::findOne), basándose en el ID del país ($model->country_id) para buscar su nombre y mostrarlo ($country->name).

    Un saludo y espero que te sea de ayuda

  4. Pingback: Gridview: agregar botones al ActionColumn | Blog de vicmonmena

  5. muchas gracias por la buena respuesta y rapida, despues de haber hecho el comentario, me puse a analisar lo de las relaciones y di con el clavo pero lo que me comenta ahora es mucho mas completo porque no estaba trabajando con el has many. me da algunos errores pero creo que ya son algunas particularidades o errores en mi proyecto. gracias una vez mas y siga el buen trabajo. saludos

  6. Fernando dijo:

    Hola, mis saludos y éxitos en este blog con explicaciones tan detalladas.
    Mi duda es la siguiente. En caso de que quisiera en la tabla Ciudad poder buscar un País sin necesidad de que se me muestre una lista desplegable cómo haría. He probado en el fichero views/city/index.php quitando la parte relacionada con country_id específicamente eliminando: ‘filter’ => ArrayHelper::map(Country::find()->all(), ‘id’, ‘name’)
    Logrando con esto que no se visualice la lista desplegable pero que a la hora de buscar no pueda recuperar los países por su nombre sino por su id. Saludos

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s