Inek

PHP y SQLite en S3

¡Tu mensaje fue recibido! Una vez que sea aprobado, estará visible para los demás visitantes.

Cerrar

27 Aug 2023

Utilizar bases de datos Sqlite almacenadas en S3 pueden ser una alternativa económica para gestionar la persistencia de nuestra aplicación en AWS. Este post es una prueba de concepto para validar este enfoque.

Requisitos previos

Para poder poner en práctica esta prueba de concepto deberás contar con PHP +8.1 instalado, con las extensiones sqlite y pdo_sqlite configuradas. Podés verificar esto último con el siguiente comando:

$ php -i | grep sqlite
Configure Command => ...
..
pdo_sqlite
sqlite3
..

Además de PHP + PDO + Sqlite, necesitaremos disponer de una cuenta de AWS en donde tengamos nuestro bucket en S3, con credenciales que nos permitan escribir en él. Podemos crear un bucket con el siguiente comando:

aws s3api create-bucket --bucket sqlite-on-s3 --profile $MY_PROFILE --region $MY_REGION

Debemos reemplazar las variables MY_PROFILE y MY_REGION con los valores adecuados. Si no sabés cuáles son esos valores, en este enlace a la documentación de AWS vas a encontrar información útil.

Para confirmar que tenés el bucket correctamente creado, deberías verlo en el listado generado por este comando:

aws s3api list-buckets --profile $MY_PROFILE --region $MY_REGION

Configurar el script main.php

Descarga el repositorio con el código de este post desde este enlace. Dicho repositorio contiene un script main.php, en donde deberás colocar el nombre del bucket creado en la sección anterior y, opcionalmente, podés indicar un nombre para el archivo sqlite.

Para ejecutar el script, nos ubicamos en la carpeta raíz del repositorio y utilizamos este comando:

AWS_PROFILE=$MY_PROFILE REGION=$MY_REGION php src/main.php

Si está todo bien configurado, la primera vez que lo ejecutemos, deberíamos ver algo así:

Fetch previous rows: 
Done.
Adding data
Done.

Si ejecutas el comando de nuevo, la salida debería ser algo similar a esto:

Fetch previous rows: 
1 - 2023-08-27 22:13:20
Done.
Adding data
Done.

Si continuás ejecutándolo, verás más entradas bajo “Fetch previous rows”.

Explicación de la prueba de concepto

La clave de la prueba está en la clase ConnectionManager. Ésta recibe como parámetros al cliente S3 de AWS, el bucket y el nombre del archivo de la base de datos.

Para ejecutar una query, las clases “cliente” de ConnectionManager deberán solicitar una conexión usando el método get:

class ConnectionManager
{
// ...
    public function get(): \PDO
    {
        if (empty($this->db)) {
            $this->downloadDatabaseFile();
            $this->initDatabase();
        }

        return $this->db;
    }
// ...

Veamos los métodos que allí se invocan:

class ConnectionManager
{
// ...
    private function downloadDatabaseFile()
    {
        try {
            $this->client->getObject([
                'Bucket' => $this->bucket,
                'Key' => $this->database,
                'SaveAs' => $this->localPath
            ]);
        } catch (S3Exception $e) {
            unlink($this->localPath);

            if (false === strpos($e->getMessage(), '404 Not Found')) {
                throw $e;
            }
        }
    }
// ...

Aquí se descarga el archivo de la base de datos desde el S3 usando el método getObject del cliente de S3 de AWS. Si el archivo no existe, se disparará una excepción S3Exception. Si la causa de la excepción es que el archivo no existe en nuestro S3, entonces continuamos la ejecución. Ante cualquier otro motivo de la excepción, la relanzamos interrumpiendo el script.

Una vez que se intentó la descarga, sea que el archivo existe o no, intentaremos conectarnos a nuestra base de datos sqlite:

class ConnectionManager
{
// ...
    private function initDatabase()
    {
        $this->db = new \PDO('sqlite:' . $this->localPath);
        $this->createTableIfNotExists();
    }

    private function createTableIfNotExists()
    {
        $sql = <<<END
CREATE TABLE IF NOT EXISTS executions (
    id INTEGER PRIMARY KEY,
    created_at TEXT NOT NULL)
END ;

        $this->db->exec($sql);
    }
// ...

Al instanciar la clase PDO, si el archivo no existe será creado. A continuación, el método createTableIfNotExists nos asegura que tras la inicialización, la base de datos tendrá la tabla requerida para interactuar.

Tras operar con la base de datos, querremos subir la nueva versión de la misma al S3 nuevamente. Esto ocurre en el método __destruct, que se ejecuta automáticamente cuando termina la vida del objeto de tipo ConnectionManager:

class ConnectionManager
{
// ...

    public function __destruct()
    {
        if (!file_exists($this->localPath)) {
            return;
        }

        $this->client->putObject([
            'Bucket' => $this->bucket,
            'Key' => $this->database,
            'SourceFile' => $this->localPath,
            'ContentType' => 'application/vnd.sqlite3',
        ]);

        $this->db = null;
        unlink($this->localPath);
    }
// ...

Si el archivo local existe, será almacenado en el S3 nuevamente y luego se elimina la copia local.

Después de la prueba de concepto

Este post es una sencilla demostración del uso de una base de datos Sqlite almacenada en un bucket S3. En caso de querer profundizar la idea para hacer más robusta nuestra aplicación, deberíamos tener en cuenta los siguientes puntos:

Lecturas útiles

¿Qué te pareció el post?

No hay comentarios.