Etiqueta: mysql

  • Cómo ejecutar phpMyAdmin en Docker

    Cómo ejecutar phpMyAdmin en Docker

    Una vez tienes dockerizada tu aplicación, lo siguiente que querrás hacer, como para terminar de dejar atrás el viejo XAMPP, será acceder a tu base de datos en forma gráfica, ¿cierto?.

    Asumiré que tienes un archivo docker-compose.yml similar a este:

    version: '3.8'
    services:
      db:
        image: 'mysql:5.7.42-debian'
        environment:
          - MYSQL_ROOT_PASSWORD=root
          - MYSQL_DATABASE=ruko
          - MYSQL_USER=ruko
          - MYSQL_PASSWORD=ruko
        restart: always
        volumes:
          - 'db-data:/var/lib/mysql'
      webserver:
        image: 'ruko_dev'
        user: 'www-data'
        build:
          context: '.'
        restart: always
        ports:
          - '8888:80'
        volumes:
          - './app/:/var/www/html/'
    volumes:
      db-data: {}

    Con lo cual, al hacer docker-compose up:

    • Tendrás acceso a tu sitio a través de http://localhost:8888
    • Cada cambio que realices se reflejará automáticamente al recargar la página

    Lo que faltaría sería poder ingresar a alguna dirección local (Por qué no http://localhost:9999) y ver algo como:

    Como de costumbre, existen varias opciones para lograrlo. La que considero más conveniente es crear un nuevo contenedor que pueda ejecutar phpMyAdmin. Veámoslo.

    phpMyAdmin en un contenedor nuevo

    Básicamente, lo que se necesita es un nuevo contenedor que contenga un servidor web y pueda conectarse a la base de datos que tiene tu entorno.

    Qué mejor para ello que usar una imagen estándar de phpMyAdmin, ¿no?

    En otras palabras, basta con ejecutar un comando del estilo:

    docker run --name phpmyadmin -d --network dockerized_rukovoditel_342_default -p 9999:80 phpmyadmin

    La red dockerized_rukovoditel_342_default corresponde a la red creada durante la ejecución de docker-compose up en mi ambiente, en el tuyo seguramente será algo diferente. En todo caso, siempre puedes usar docker network ls para averiguarlo.

    Con esto ya tenemos lo suficiente como para ingresar a http://localhost:9999 y trabajar cómodamente con phpMyAdmin.

    Claro que tener que usar este comando no es lo más práctico del mundo, ¿no?

    Completemos el trabajo metiendo la nueva configuración dentro del docker-compose.yml.

    phpMyAdmin en un contenedor nuevo manejado por docker-compose

    Como podrás imaginar, sólo se trata de traducir la llamada directa al cliente de docker a la sintaxis YAML utilizada por docker-compose. Es decir, se trata de sumar esta definición a la sección services del archivo docker-compose.yml:

      pma:
        image: 'phpmyadmin'
        ports:
          - '9999:80'
        restart: always

    Para probarlo todo basta con ejecutar docker-compose down y docker-compose up.

  • ¿Cuál es la diferencia entre PDO y MySQLi?

    ¿Cuál es la diferencia entre PDO y MySQLi?

    Si me decís que estás desarrollando una aplicación web usando PHP y no me das más datos, cerrando los ojos diría que estás usando una base de datos MySQL y, seguro que en el 80% de los casos acertaría.

    Es un hecho: PHP y MySQL son un dúo muy común en este mundillo.

    Lo que no está tan definido es cuál es el mejor modo de conectarse a MySQL utilizando PHP.

    A priori destacan dos:

    Ambos pueden usarse para armar CRUDs, reportes y cualquier otra necesidad que implique comunicar una aplicación PHP con una base de datos MySQL.

    Entonces… ¿cómo elegir?

    Para hacerte (algo) más fácil la tarea te voy a comentar brevemente de qué se trata cada uno.

    Qué es MySQLi

    MySQLi es una extensión de PHP creada con el propósito de facilitar la comunicación entre un script PHP y un servidor MySQL.

    Es la heredera de la vieja y querida extensión MySQL que ya ha quedado obsoleta.

    MySQLi hizo su aparición junto con la versión 4.1 de MySQL.

    Lo más probable es que nunca te cruces con un servidor que utiliza una versión anterior (Al día en que estoy escribiendo este artículo la versión de MySQL es la 8.0) pero… si llegara a pasar, vas a tener que arreglártelas con la extensión mysql (sin la «i»).

    Su nombre precisamente viene de MySQL improved (Mejorada).

    Una característica interesante de esta extensión es que muestra una API Orientada a Objetos y también una estructurada.

    Por ejemplo, para establecer una conexión a un servidor podés usar tanto:

    <?php
    $link = mysqli_connect("localhost", "root", "", "mydb");

    Como

    <?php
    $mysqli = new mysqli('localhost', 'root', '', 'mydb');

    Si estás migrando un sistema que usaba la vieja extensión y no tenés mucho tiempo para hacer grandes cambios es trivial actualizar al uso de la nueva librería: basta agregar una «i» al nombre de las funciones que venías usando.

    Claro que, si querés dar un salto, te recomiendo hacer el esfuerzo de escribir el código usando objetos.

    Qué es PDO

    PDO también es una extensión pero, a diferencia de MySQLi, PDO está diseñada como una capa de abstracción sobre una amplia cantidad de motores específicos.

    Para que se entienda mejor, esta es la relación entre PDO y los motores específicos:

    Cuál es la ventaja de usar PDO en lugar de MySQLi

    La ventaja principal de usar PDO en lugar de MySQLi es que, llegado el caso de decidir migrar a un motor de base de datos diferente del elegido originalmente, el cambio es trivial.

    Por ejemplo, si tuvieses que pasar de MySQL a SqlServer deberías cambiar:

    $conn = new PDO('mysql:dbname=mydb;host=127.0.0.1');

    Por

    $conn = new PDO('mssql:dbname=mydb;host=127.0.0.1');

    Y el resto de tu código seguiría funcionando sin alteraciones.

    Cuál es la ventaja de usar MySQLi en lugar de PDO

    Pero si PDO es tan potente y versátil… ¿no hay lugar para MySQLi en la actualidad?

    La verdad es que sí lo hay (a diferencia de lo que muchos creen).

    La versatilidad de PDO es un arma de doble filo.

    Como sucede con cualquier solución genérica, quien mucho abarca poco aprieta 🙂

    En términos técnicos, lo que ocurre es que PDO no puede aprovechar las pequeñas diferencias existentes entre los diferentes motores, lo cual lo hace menos eficiente que MySQLi en ciertas situaciones.

    Por otro lado, una capa más de abstracción agrega más overhead.

    Cómo decidir si usar PDO o MySQLi

    No quiero que te vayas de aquí con una confusión mayor de la que traías al comienzo, así que preparé este sencillo diagrama de flujo para ayudarte a tomar la decisión, espero te sirva:

  • Cómo exportar una tabla de MySQL a Excel usando PDO

    Cómo exportar una tabla de MySQL a Excel usando PDO

    Seguro que te ha pasado algo como esto: creaste una aplicación con su base de datos, con unas funcionalidades espectaculares, fantásticos reportes y al momento de la demo… la pregunta tan temida:

    «¿Cómo puedo hacer para llevar toda esta información a Excel?»

    Y en tu cabeza suena:

    «¿A Excel? ¿En serio? Pero si se puede hacer todo mucho mejor con esta aplicación… ¿para qué querrías usar una planilla de cálculo?»

    Creeme, no estás solo, a todos nos ha pasado.

    La triste realidad es que difícilmente vayas a ganar la batalla… más vale amigarte con el Excel (y ganar algo de dinero mientras tanto, ¿no?).

    Una de las razones más comunes que se escuchan por ahí es que a través de Excel es fácil compartir la información con otros sistemas (Personalmente elegiría una integración basada en webservices, pero… el cliente manda).

    Otra razón que, aunque a veces duela, la veo más entendible es la facilidad para analizar datos que tiene Excel (La posibilidad de aplicar filtros, sumatorias y demás).

    Pero como este no es un blog de Excel si no de Programación, pasemos a lo nuestro.

    En este artículo te voy a mostrar los pasos que tendrás que dar para generar archivos que puedan ser abiertos con Excel a partir de tu base de datos MySQL.

    Encontrarás ejemplos de código que, con pocas modificaciones, podrás adaptar a tu escenario particular.

    ¡Acompañame!

    Conectarse a MySQL usando PDO

    Lo primero que necesitarás para realizar esta exportación es conectarte a tu base de datos.

    Siendo que se trata de MySQL puedes usar la librería específica (mysqli) o la genérica (PDO).

    Personalmente, prefiero ir por PDO porque si eventualmente necesito cambiar de motor de BD es muy fácil hacerlo.

    Todo comienza por la creación de un objeto:

    <?php
    
    $db = new PDO('mysql:host=localhost;dbname=mydb', 'root', '');

    Con este código estoy intentando hacer una conexión a una base de datos MySQL instalada en la misma computadora donde se está corriendo el script.

    Digo intentando porque, como siempre, algo podría salir mal… para este ejemplo voy a dejarlo así, pero lo correcto sería incluir este código dentro de un bloque try…catch.

    (Si no tenés muy claros los conceptos de objetos, excepciones y/o PDO este libro puede ayudarte 😉

    Una vez realizada la conexión debemos hacer alguna consulta que nos permita obtener aquellos registros que queremos exportar.

    Realizar una consulta usando PDO

    Existen varios modos de realizar la consulta mediante PDO, pero hay algo de lo que no podrás escapar: escribir el SQL correspondiente.

    Usualmente lo que yo hago es algo como:

    $sql = "SELECT * FROM table WHERE field > 10";
    
    $results = $pdo->query($sql, PDO::FETCH_NUM);

    Nuevamente, en un caso real habría que verificar si la consulta se ejecutó o hubo algún error antes de avanzar… para no hacer un post kilométrico lo dejo aquí.

    Para darle algo de formato al resultado que vamos a generar vamos a necesitar los nombres de los campos (que, si usamos un SELECT * no siempre serán los mismos!)

    Obtener los nombres de los campos usando PDO

    $columns = [];
    
    for ($i = 0; $i < $results->columnCount(); $i++) {
        $columns[] = $results->getColumnMeta($i)['name'];
    }

    Una vez obtenidos los registros y los nombres de las columnas a exportar podemos comenzar a generar la respuesta.

    Y aquí tenemos varias opciones.

    Independientemente de cuál sea la que elijas para generar el resultado, seguramente quieras forzar la descarga del mismo, veamos cómo lograrlo.

    Forzar la descarga de un archivo

    Para obligar al navegador a guardar el resultado devuelto por el servidor en un archivo en el disco local del visitante debe usarse la función header:

    header("Content-Type:$contentType"); 
    header("Content-Disposition:attachment;filename=output.$outputFileExtension"); 

    A continuación te mostraré tres opciones diferentes para generar la respuesta.

    Las variables $contentType y $outputFileExtension dependerán de cuál de las opciones disponibles quieras utilizar.

    Generar un archivo CSV y abrirlo en Excel

    Esta es probablemente la manera más sencilla de exportar los resultados, usando un archivo de valores separados por comas (CSV).

    header("Content-Type:application/csv"); 
    header("Content-Disposition:attachment;filename=output.csv"); 
    
    $outputFile = fopen('php://output', 'w+'));
    
    fputcsv($outputFile, $columns);
    foreach ($results as $result) {
            fputcsv($outputFile, $result);
    }

    Este código permitirá que el usuario reciba un archivo separado por comas que podrá ser abierto usando Excel y eventualmente guardado en formato nativo (xls, xlsx, etc…).

    Generar un archivo HTML y abrirlo en Excel

    Una característica interesante del Excel (Y algunas otras aplicaciones tipo planilla de cálculo modernas) es que pueden interpretar archivos HTML que tengan estructuras de tablas y mostrarlos como planillas.

    En este caso el código de la generación de la salida sería algo como:

    header("Content-Type:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); 
    header("Content-Disposition:attachment;filename=output.xls"); 
    
    $outputFile = fopen('php://output', 'w+'));
    
    ?>
    <table>
            <thead>
                    <tr>
                    <?php foreach($columns as $column): ?>
                            <th><?php echo $column; ?></th>
                    <?php endforeach; ?>
                    </tr>
            </thead>
            <tbody>
                    <?php foreach( $results as $result ): ?>
                    <tr>
                            <?php foreach ($result as $value): ?>
                            <td><?php echo $value; ?></td>
                            <?php endforeach; ?>
                    </tr>
                    <?php endforeach; ?>
            </tbody>
    </table>

    Generar un archivo Excel usando PHP

    Si bien estas dos opciones pueden resultar aceptables en una gran cantidad de casos, existen otros en los que realmente se necesita exportar a un formato específico, como XLSX.

    Estos archivos se basan en XML (es decir, en texto) con lo cual podrías generarlos usando simples strings (O a lo sumo valiéndote de SimpleXMLElement) pero… ¿para qué reinventar la rueda no?

    Hay una librería realmente buena que podés usar: PhpSpreadsheet.

    Esta librería te permite tanto escribir como leer archivos con formato específico para muchas planillas de cálculo (Excel, LibreOffice, etc…).

    Una característica que la hace sumamente atractiva es que presenta una API orientada a objetos realmente simple de usar:

    $spreadsheet = new Spreadsheet();
    
    $activeSheet = $spreadsheet->getActiveSheet();
    
    foreach($columns as $i => $column) {
            $activeSheet->setCellValueByColumnAndRow($i + 1, 1, $column);
    }
    
    foreach($results as $result) {
            $activeSheet->insertNewRowBefore($activeSheet->getHighestRow() + 1);
            foreach ($result as $k => $field) {
                    $activeSheet->setCellValueByColumnAndRow($k + 1, $activeSheet->getHighestRow(), $field);
            }
    }
    
    $writer = IOFactory::createWriter($spreadsheet, 'Xls');
    $writer->save('php://output');

    Un detalle importante: para usar esta librería hay que instalarla primero (Preferentemente usando composer).

    Qué se puede hacer con Excel usando PHP

    Así como es posible exportar información hacia un archivo Excel es perfectamente posible importar información contenida en un archivo Excel hacia una base de datos MySQL o manipular un archivo existente.

    La librería PhpSpreadsheet permite realizar prácticamente cualquier operación que Excel soporte, todo lo que se requiere es leer un poco de documentación 😉

  • Cómo compartir la conexión a MySQL entre scripts PHP

    Cómo compartir la conexión a MySQL entre scripts PHP

    Leo frecuentemente comentarios como:

    Al guardar la conexión a MySQL usando serialize y unserialize me da errores

    O

    Guardo la conexión a la BD en una variable de sesión pero después cuando consulto ese valor desde otra página no existe

    Nadie quiere tener que abrir una nueva conexión a la base de datos cada vez que se ejecuta una página PHP. Compartir en X

    En un contexto de alta concurrencia esto puede suponer un gran desperdicio de recursos e incluso convertirse en un cuello de botella.

    Sin embargo, la solución no es guardar la conexión dentro de la sesión… de hecho, eso no va a funcionar.

    Se trata de un caso muy particular, pero existe una alternativa.

    Qué son las conexiones permanentes a MySQL

    En el uso normal de las conexiones a MySQL (usando, por ejemplo mysqli_connect), al finalizar la ejecución del script se cierran automáticamente las conexiones que hayan sido abiertas.

    Si tenemos en cuenta el modelo de ejecución de php notaremos que habrá un gran overhead con toda esa apertura/cierre de conexiones.

    Para evitar este problema PHP dispone de conexiones permanentes.

    Se trata de conexiones que, a diferencia de las comunes, continúan existiendo luego de que el script ha finalizado su ejecución.

    Cómo se crean conexiones permanentes de PHP a MySQL

    En general, existen dos modos de vincular PHP y MySQL:

    1. MySQLi
    2. PDO

    La principal diferencia entre ellas es que MySQLi es una librería específica (es decir, sólo puede trabajar con MySQL) mientras que PDO es genérica (Puede trabajar con diferentes motores de bases de datos).

    Ya que estamos hablando de MySQL en este post voy a explorar el uso de MySQLi.

    La forma de crear una conexión persistente es simple, sólo se necesita agregar antes del nombre del host el string p: ejemplo:

    <?php
    
    $conn = mysqli_connect("p:localhost", "root", "rootpwd");

    ¡Y listo! Con este sencillo código puedes olvidarte del problema de abrir y cerrar conexiones a MySQL en cada script de tu sitio.

  • Implementación de roles basada en PHP y MySQL

    Implementación de roles basada en PHP y MySQL

    Una pregunta que veo a menudo:

    Estoy haciendo un inicio de sesión en php. el usuario solo debe ingresar su nombre y sera re direccionado dependiendo del rol que tenga.


    Estoy realizando un sistema en php y mysql quisiera saber cómo trabajar con múltiples sesiones como por ejemplo que tenga una cuenta de administrador y pueda trabajar con toda las páginas y tener cuenta de usuario que algunos vean cierta cantidad de páginas y realizar pocas funciones en el sistema


    ¿Cómo debo crear una sesión para el administrador? ¿En qué lugar de la aplicación se tiene que reflejar?
    El usuario normal no debe ver esa parte que le corresponde al Administrador.


    quiero ingresar roles de usuario en mi código pero no se como hacerlo.


    Voy a crear una aplicación web para unas personas que llevan el control de unas muestras geológicas, básicamente intervienen dos grupos de usuarios el gerente del área y sus especialistas, por lo tanto no pueden tener los mismos privilegios.

    La necesidad de tener diferentes secciones del sitio disponibles para diferentes roles o niveles de usuario es sumamente común.

    En este artículo mostraré cómo podría implementarse un esquema como este usando PHP y MySQL.

    Aclaración: en mi caso personal, para resolver este problema utilizaría Symfony, pero en este post utilizaré PHP puro para hacer la solución más generalmente aplicable

    Cómo guardar los roles en la base de datos

    Mi sugerencia siempre es comenzar por definir el modelo de datos.

    La base de datos constituye los cimientos de cualquier aplicación web y si éstos no son sólidos, por más magia que tenga nuestro PHP, CSS, JavaScript y demás va a ser difícil terminar con algo mucho mejor que esto:

    Así que… mejor pisar sobre seguro.

    La primera pregunta que debés responder es: ¿cada usuario tendrá un único rol?

    Si la respuesta es sí el esquema de la base de datos es bastante simple, sólo se trata de agregar un campo a la tabla users: role.

    Aquí se abre una segunda bifurcación: ¿de qué tipo debe ser el campo role?

    La idea más natural es crearlo como string (o tal vez enumerado).

    Mi sugerencia, sin embargo, es hacer algo un poco más complicado:

    • Crear una nueva tabla roles con dos campos:
      • id
      • nombre
    • Agregar un campo role_id (en lugar de role) a la tabla users como una clave foránea a la tabla roles

    En definitiva, este sería el código SQL de la creación de las tablas:

    CREATE TABLE `roles` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `name` (`name`)
    );
    
    CREATE TABLE `users` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `email` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
      `password` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
      `role_id` int(10) unsigned DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `email` (`email`),
      KEY `fk_roles` (`role_id`),
      CONSTRAINT `fk_roles` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`)
    );

    De esta forma el sistema está preparado para incorporar nuevos roles sin necesidad de alterar la estructura de la base (Y por lo tanto, posibilitar hacerlo desde el front-end sin intervención del desarrollador).

    En el caso de que los usuarios puedan tener más de un rol simultáneamente la estructura sería un poco más compleja: deberá incluir una tabla intermedia para implementar la relación N-a-M:

    -- Queda igual que en el caso anterior
    CREATE TABLE `roles` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `name` (`name`)
    );
    
    -- Se elimina el campo role_id
    CREATE TABLE `users` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `email` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
      `password` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `email` (`email`)
    );
    
    CREATE TABLE `users_roles` (
      `user_id` int(10) unsigned NOT NULL,
      `role_id` int(10) unsigned NOT NULL,
      PRIMARY KEY (`user_id`,`role_id`),
      KEY `fk_roles_2` (`role_id`),
      CONSTRAINT `fk_roles_2` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`),
      CONSTRAINT `fk_users` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
    );

    Para no complicar mucho el ejemplo voy a tomar el caso de un rol por usuario aunque no cambiará mucho si se trata del otro (Tal vez lo escriba en un próximo artículo).

    Bien, la base de datos está, ahora es momento de ver cómo hacer el código PHP.

    Cómo redireccionar al usuario según su rol

    El siguiente desafío consiste en, una vez haya ingresado el usuario, redirigirlo a la sección del sitio que le corresponda según su rol.

    Comencemos por un simple formulario de login:

    <html>
    <body>
      <form action="processLogin.php" method="post">
         <input type="text" name="username"/>
         <input type="password" name="password"/>
         <input type="submit" value="Ingresar"/>
      </form>
    </body>
    </html>

    Y el archivo del backend

    <?php
    
    $username = $_POST['username'];
    $password = $_POST['password'];
    
    $sql = "SELECT u.id, r.name AS role, password FROM users u INNER JOIN roles r ON r.id = u.role_id WHERE username = '$username';";
    
    // Conectar a la base de datos
    // ejecutar la consulta
    // $result contiene el resultado de la consulta
    
    if (password_verify($result['password'], password_hash($password, PASSWORD_BCRYPT))) {
       $startingPage = [
          'admin' => 'admin_home.php',
          'user' => 'user_home.php',
       ];
    
       $nextPage = array_key_exists($result['role'], $startingPage) ? $startinPage['role'] : 'user_home.php';
       if (array_key_exists($result['role'], $startingPage)) {
          $nextPage = $startinPage[$result['role']];
       } else {
          $nextPage = $startinPage['user'];
          error_log('There is no starting page for role '.$result['role']);
       }
       session_start();
       $_SESSION['user_id'] = $result['id'];
       $_SESSION['role'] = $result['role'];
       header('Location: '.$nextPage);
    } else {
       header('Location: login.html');
    }

    Aquí estoy usando las funciones password_verify y password_hash para hacer un login seguro.

    Y con esto hemos logrado dirigir a cada persona a la sección que le corresponde…

    Lo que nos falta es dejar afuera a los que quieran ir a mirar donde no les corresponde 😉

    Cómo restringir el acceso a una sección del sitio según el rol del usuario

    Una forma simple de restringir el acceso para alguien que no se logeó la podés leer acá.

    La idea de esta sección es ampliar un poco sobre esa misma línea y permitir el acceso sólo para aquellos usuarios cuyo rol está habilitado:

     <?php
    
    // admin_home.php
    
    session_start();
    
    if (!array_key_exists('user_id', $_SESSION)) {
       header('Location: login.html');
       die;
    }
    
    $allowedRoles = ['admin'];
    
    if (!array_key_exists('role', $_SESSION) || !in_array($_SESSION['role'], $allowdRoles)) {
       header('Location: login.html');
       die;
    }
    ?>
    <h1>Bienvenido amind!</h1>

    Habrás observado que el protagonista de esta pequeña novela es el array $_SESSION.

    Si todavía no lo tienes super claro, te sugiero continuar leyendo por aquí.

  • Cuál es la mejor forma de almacenar fechas en MySQL

    Cuál es la mejor forma de almacenar fechas en MySQL

    ¿Tenés que desarrollar una aplicación que maneja fechas?

    Tal vez un portal para reserva de turnos, o quizás algún sistema de membresías por tiempo limitado o por qué no un programa que le pregunte al visitante su fecha de nacimiento y le diga el signo del horóscopo chino al que pertenece.

    Más allá de cuál sea el objetivo de la aplicación en algún lado vas a necesitar almacenar fechas.

    Y ese lado será probablemente una base de datos relacional y, más aún, si estás usando PHP, seguramente sea MySQL.

    Existen varias opciones que podrías usar para definir el tipo de datos del campo en cuestión y la decisión puede no ser trivial.

    Usar un VARCHAR para almacenar un dato fecha

    Si bien técnicamente podrías guardar una fecha en un campo de tipo VARCHAR (O cualquier otro tipo string), esto te hará difícil resolver algunos problemas como por ejemplo ordenar un set de resultados en función de la línea de tiempo.

    Es decir, si una fecha es '2020-02-01' y la otra es '19000-02-01' el motor te dirá que la primera es posterior a la segunda.

    Esto sucede porque, en lugar de tomar el valor como una fecha, se lo está tratando como una simple cadena de caracteres y, por lo tanto, se está aplicando el orden lexicográfico.

    Esto significa que para comparar se están tomando los caracteres y se comparan uno a uno, de este modo:

    20200201
    190000201

    Por otra parte, como se trata de cadenas, el motor no será capaz de determinar que '2020-13-35' no es un valor aceptable.

    Así que… no te lo recomiendo

    Usar un TIMESTAMP para almacenar un dato fecha

    Un segundo tipo de datos que podrías usar para guardar fechas es el TIMESTAMP.

    Este seguramente va a funcionar mejor que el VARCHAR, pero tampoco es el ideal.

    El TIMESTAMP es, internamente, un número muy grande que mide cuántos segundos pasaron desde segundo uno de UNIX (1970-01-01 00:00:01).

    Usualmente este tipo de datos se utiliza para operaciones que requieren altísima precisión, como procesos de tiempo real… un poco exagerado cuando se quiere saber la fecha en que una persona recibió su último aumento de sueldo, ¿no?

    Usar un DATE para almacenar un dato fecha

    La mejor opción es usar un campo de tipo DATE.

    Este tipo de datos modela una fecha mediante una estructura que tiene separados los componentes del mes, día y año.

    Si bien al momento de visualizarlo no será distinguible de un VARCHAR que tenga el mismo contenido, a nivel funcional será muy diferente.

    Entre otras, el tener los datos almacenados usando el tipo correcto permitirá realizar consultas como:

    SELECT * FROM usuarios WHERE subscription_date BETWEEN '2020-01-10' AND '2020-01-20';

    Y obtener como resultado aquellos usuarios que se han suscrito a nuestro sitio entre el 10 y el 20 de Enero de 2020.

    Usar un DATETIME para almacenar un dato fecha

    Si necesitas almacenar, además de la fecha, la hora exacta en que sucedió algo, lo mejor es utilizar el tipo de datos DATETIME que se comporta igual que DATE pero agregando la información de horas, minutos y segundos.

  • Cómo resolver el Problema del N+1 en PHP y MySQL

    Cómo resolver el Problema del N+1 en PHP y MySQL

    Qué es el problema del N+1

    Se trata de un problema de optimización de consultas a una base de datos.

    Imagina que tienes una base de datos con dos tablas:

    • Users
    • PhoneNumbers

    Donde cada usuario puede tener muchos números telefónicos (Relación 1:N).

    Ahora, imagina que tienes que mostrar un listado de todos los usuarios junto con sus números de teléfono.

    Una forma de resolverlo sería utilizar una consulta tipo:

    SELECT * FROM users;

    Y luego:

    foreach ($users as $user) {
          $sql = "SELECT * FROM phone_numbers WHERE user_id = {$user->getId()};";
    
         ...
    }

    Lo que sucede aquí es que realizamos una consulta para obtener todos los usuarios y luego, una más por cada uno.

    De ahí que el número total de consultas que se realizan es N (Número de usuarios) + 1.

    Esto puede representar un gran problema cuando N es grande… especialmente cuando el tiempo que tarda cada consulta es significativo (Por ejemplo porque la base de datos está en un servidor remoto y hay que contar con los tiempos de transferencia de red).

    Solución basada en JOIN

    Una primera solución sería utilizar un join:

    SELECT * FROM users u INNER JOIN phone_numbers p ON p.user_id = u.id;

    Aquí el problema es que el resultado tendrá mucha información duplicada (Todos los datos del usuario estarán repetidos en cada fila), lo cual puede traernos inconvenientes con la memoria.

    Solución de dos consultas

    Una solución más eficiente se basa en la realización de dos consultas.

    Los pasos son los siguientes:

    1. Buscar los objetos principales
    2. Obtener los Ids de dichos objetos
    3. Buscar los objetos relacionados para aquellos Ids obtenidos en 2
    4. Mostrar el resultado combinado
    SELECT * FROM user;
    $ids = array_map(function(User $u) { return $u->getId(); }, $users );
    $phone_numbers = $db->query("SELECT * FROM phone_numbers WHERE user_id IN ('.implode(', ', $ids).')");
    
    foreach ( $users as $user ) {
         $str = $user->getName() . implode(',', array_filter($phone_numbers, function($p) use ($user) {
                  return $p['user_id'] == $user->getId();
         }));
    }

    En esta solución la cantidad de consultas realizadas siempre es fija: 2.

    Puede parecer algo más laborioso del lado del PHP, pero ten en cuenta que los tiempos de procesamiento siempre son significativamente inferiores respecto de los de comunicación vía red u otro tipo de operaciones de entrada/salida, con lo cual, vale la pena el esfuerzo extra 😉

  • Cómo armar una tabla pivot con PHP y MySQL

    Cómo armar una tabla pivot con PHP y MySQL

    Hurgando en las profundidades de la Internet me encontré con una pregunta muy interesante.

    El autor comentaba que tenía una tabla con esta pinta:

    Y quería, mediante una consulta a MySQL, obtener un resultado de este tipo:

    Básicamente el desafío era transformar datos horizontales (los valores de la columna «endpoint» para cada fila) en verticales, es decir, columnas de la respuesta.

    No es algo que se vea todos los días, cierto, pero… ¿cómo rechazar un desafío semejante? 🙂

    Inmediatamente me vino a la mente el concepto de Tabla Pivot que manejan las planillas de cálculo.

    Así que me arremangué y escribí este SQL:

    SELECT FROM_UNIXTIME(FLOOR((UNIX_TIMESTAMP(fecha_hora))/60)*60) as fecha_hora,
        SUM(
    		CASE
    		  WHEN endpoint = 50
    		   THEN numero_personas
    		   ELSE 0
    		END
        ) as suma_personas_ep50,
        SUM(
    		CASE
    			WHEN endpoint = 51
    			THEN numero_personas
    			ELSE 0
    		END
        ) as suma_personas_ep51
    	FROM t
    	GROUP BY FROM_UNIXTIME(FLOOR((UNIX_TIMESTAMP(fecha_hora))/60)*60)
    	;

    Con esto obtuve el resultado esperado. ¡Exito!

    Pero… inmediatamente surgió la pregunta en mi cabeza: ¿Qué pasará cuando se agreguen nuevos endpoints?

    Pues… ¡usemos un poco de PHP!

    <?php
    
    try {
    	$pdo = new PDO("mysql:dbname=escuelait;host=localhost", "root", "1234");
    } catch ( PDOException $e ) {
    	die ( $e->getMessage() );
    }
    
    $sql = "SELECT DISTINCT(endpoint) FROM t;";
    
    $endPoints = $pdo->query($sql, PDO::FETCH_COLUMN,0)->fetchAll();
    
    $templateSQL = "SUM(
    		CASE
    		  WHEN endpoint = |ENDPOINT|
    		   THEN numero_personas
    		   ELSE 0
    		END
        ) as suma_personas_ep|ENDPOINT|";
    	
    $endPointsSQL = implode( ",".PHP_EOL, array_map( function( $endPoint ) use ( $templateSQL ) {
    	
    	return preg_replace( '/\|ENDPOINT\|/', $endPoint, $templateSQL ) ;
    }, $endPoints ) );
    
    $finalSQL = "SELECT FROM_UNIXTIME(FLOOR((UNIX_TIMESTAMP(fecha_hora))/60)*60) as fecha_hora,
        $endPointsSQL
    	FROM t
    	GROUP BY FROM_UNIXTIME(FLOOR((UNIX_TIMESTAMP(fecha_hora))/60)*60)
    	;";
    	
    echo $finalSQL.PHP_EOL;

    Este script realiza dos consultas:

    1. La primera para obtener los endpoints existentes
    2. La segunda para generar la tabla dinámica

    El truco está en generar el string SQL en forma dinámica.

    No es la sintaxis más utilizada pero… puede serte útil alguna vez 😉

  • Cómo eliminar registros antiguos usando PHP y MySQL

    Me llega este correo de un ex alumno:

     

    Mauro como estás? Espero que muy bien!

    Sigo con el proyecto del centro cultural y quisiera saber si hay alguna forma de generar un script en sql que me borre los eventos que cargue a los cuales ya les haya pasado la fecha.
    Saludos,

     

    Si quisiéramos adherir estrictamente a la pregunta, la respuesta sería «Sí»… pero sería un poco mala persona si lo dejara ahí, ¿cierto? 🙂

    Voy a profundizar un poco para no dejar a nadie con la intriga.

    Una forma de resolver el problema desde sql (Asumo que se trata de MySQL) sería generar un procedimiento almacenado o directamente tener a mano una consulta que hiciera lo que se busca: borrar los eventos a los que les haya pasado la fecha.

    Pasando un poco en limpio, se cuenta con una tabla de eventos que tiene, al menos, un campo fecha. Para simplificar un poco el escenario, asumiré que el campo fecha es efectivamente de tipo date o datetime (Se puede resolver aún si no es este el caso, pero sería bastante más complejo).

    Lo que necesitamos es saber cuáles de estos eventos tienen una fecha que sea anterior a la actual, para lo cuál la pregunta es ¿cómo podemos saber qué día es hoy usando SQL? MySQL tiene para esto una función muy útil: CURDATE().

    De modo que todo el problema (o bueno, una gran parte) se limita a ejectuar una consulta de tipo:

    DELETE FROM eventos WHERE fecha < CURDATE()

    Con esto se logra eliminar todos los registros de la tabla eventos cuya fecha sea anterior a la actual… claro que esta consulta debe ser ejecutada periódicamente si queremos que el sistema se autodepure.

    Una buena forma de lograr este segundo propósito es crear un script que corra mediante un cronjob.

    Un modo muy simple de crear este script es utilizar la interface de línea de comandos de MySQL. Ejemplo:

    #!/bin/sh
    
    mysql -u centro -pcentro -e 'DELETE FROM eventos WHERE fecha < CURDATE()'

    Todo esto se puede guardar en un archivo limpiar_eventos.sh darle permisos de ejecución y agregarlo al directorio /etc/cron.daily/ y listo, todos los días se ejecutará este script y la tabla de eventos se encontrará limpia constantemente.

    Algunas consideraciones

    1. Me tomé la libertad de asumir que el script (y la aplicación entera) estarán alojadas en algún servidor tipo POSIX (Linux, OpenBSD, etc…), en caso de usar Windows no es que cambie demasiado (Será un .bat en lugar de un .sh) pero como hace mucho que dejé de trabajar con Windows no estoy 100% familiarizado con cómo crear tareas programadas (En alguna época el Panel de Control tenía algo que ver…).
    2. Si bien este script va a funcionar OK no es muy elegante y, sobre todo, no me gusta mucho el tema de dejar lógica de la aplicación fuera del propio código. Se podría hacer un script PHP que ejecute esta misma consulta (Puntos extra si se hace a través de las mismas clases que maneja la aplicación, por ejemplo usando un comando de consola de Symfony)

    Más información

    La combinación PHP+MySQL es una de las más comunes que se encuentran en la web. Si ya conocés algo de PHP y te interesa avanzar hacia el desarrollo de aplicaciones comerciales (Carritos de compra, clasificados o ese tipo de sitios) este curso te puede resultar interesante.

  • Backups de tu MySQL almacenados en Google Drive

    Backups de tu MySQL almacenados en Google Drive

    Cuando una aplicación entra en producción (si no antes), resulta clara la necesidad de realizar backups.

    Aunque tu código sea una obra de arte digna del Louvre, lo realmente importante son los datos que generan los usuarios.

    Por más que estés usando hostings virtualmente irrompibles (Como Digital Ocean), nunca podés ser demasiado precavido.

    Hay varias formas de resolver este problema, voy a nombrar algunas (De más fácil a más compleja).

    Cómo hacer backups usando MySQL WorkBench

    Desde la pantalla principal (Una vez conectado a un servidor)

    Arriba a la izquierda encontrás el menú «Management»:

    De ahí tenés la opción «Data Export»:

    Eso te lleva a la pantalla de selección de los objetos que vas a querer exportar:

    Seleccioná la base de datos y las tablas:

    Importante: seleccioná la opción «Export to self contained file»:

    De otro modo, la exportación se hará a un archivo por tabla (Lo que simplemente complicará el proceso de restauración).

    Por último, debés iniciar el proceso de exportación:

    Y listo! Lo que obtendrás será un archivo con las instrucciones SQL para crear tu base de datos tal y como está en tu servidor:

    Este proceso es simple pero tiene dos inconvenientes principales:

    1. Es un proceso manual (Es decir, alguien debe acordarse de hacerlo)
    2. Sólo funcionará si tenés disponible una terminal gráfica (O acceso remoto usando una, lo cual es algo poco usual).

    Cómo hacer backups usando MySQLDump

    Una herramienta que viene dentro del paquete MySQL es MysqlDump.

    Se trata de una sencilla utilidad de línea de comandos cuyo objetivo es simplemente realizar volcados (dumps) de bases de datos.

    Si alguna vez usaste el cliente de línea de comandos de MySQL (el comando mysql), la forma de utilización de mysqldump no debería resultarte ajena: se especifica el servidor, la base de datos y algunas opciones más:

    Y se obtienen las sentencias sql requeridas para crear una base de datos igual que la que tenemos ahora:

    Claro que… no es muy útil tenerlo todo en la pantalla, ¿cierto?

    ¡A no preocuparse! Basta con redireccionar la salida standar y estás listo:

    Y si querés hacer algo realmente bueno podés usar un poco más de la magia de POSIX y hacer algo como:

    Y terminar con un archivo comprimido con la fecha de hoy como parte del nombre como para identificarlo rápidamente.

    Este enfoque sigue siendo manual pero:

    1. No requiere de consola gráfica (Es más probable que no tengas problemas para usarlo en tu servidor)
    2. Es fácilmente scripteable

    El último punto es lo que permite justamente sacar de la ecuación a los humanos:

    Cómo automatizar los backups de la base de datos

    Si conocés la utilidad cron te habrás imaginado ya que la verdadera potencia de este esquema está en la realización automática (y periódica) de los backups.

    Así que, si te tomás el trabajito de crear un script con el comando como el que estaba antes:

    Le das permisos de ejecución:

    chmod +x backup.sh

    Y lo ponés dentro de un cronjob:

    Ya te podés olvidar de entrar a hacer los backups al servidor.

    No está mal, ¿cierto? ¿Qué falta? ¡Ah! Sí… ¿dónde deberías guardar los backups?

    Cómo almacenar los backups en Google Drive

    Si llegaste hasta acá debe ser que te interesa mucho tener bien resguardada la información de tus clientes.

    La frutilla del postre es, aparte de realizar los backups en forma automatizada, almacenarlos en algún lugar seguro (Distinto de tu hosting).

    Existen varias alternativas, pero una bastante al alcance de cualquiera es usar una cuenta de Google o Office 365.

    Con este script guardamos los backups de las últimas 5 semanas en nuestra cuenta de Google (Todo gracias a la ayudita de un cliente de GoogleDrive para línea de comandos).

    #!/bin/bash
    backup_dirs='/var/lib/mysql'
    today=`date -I`
    backup_gdrive_path='/opt/backups/'
    
    tar -zcf ${backup_gdrive_path}/prod-${today}.tgz $backup_dirs
    old_backups=`ls -1r $backup_gdrive_path | awk 'NR <= 5 { next} { print }'`
    
    if ! [ "$old_backups}x" == "x" ]; then
     for f in $old_backups; do
     rm -f ${backup_gdrive_path}/$f
     done
    fi
    
    cd $backup_gdrive_path
    drive push -no-prompt -quiet
    drive emptytrash -no-prompt -quiet

    Y por supuesto… ¡no olvides ponerlo como cronjob!

    Y ahora sí, tenés todo lo necesario para montar un sistema de backups remotos automatizados para tu sitio.