Guía de Flask-MongoEngine en Python

En esta guía, veremos cómo usar el contenedor Flask-MongoEngine de MongoEngine para integrar y crear una aplicación CRUD en Flask y Python con MongoDB.

Introducción

Crear una aplicación web casi siempre significa tratar con datos de una base de datos. Hay varias bases de datos para elegir, dependiendo de su preferencia.

En esta guía, veremos cómo integrar una de las bases de datos NoSQL más populares, MongoDB, con el micromarco Flask.

En esta guía, exploraremos cómo integrar MongoDB con Flask usando una biblioteca popular - MongoMotor, y más específicamente, su contenedor - ***Flask-MongoEngine ***.

Alternatively, you can integrate MongoDB con Flask-PyMongo.

Flask-MongoEngine

MongoEngine es un ODM (Mapeador de documentos de objetos) que asigna clases de Python (modelos) a documentos de MongoDB, lo que facilita la creación y manipulación de documentos programáticamente directamente desde nuestro código.

Instalación y configuración

Para explorar algunas de las funciones de MongoEngine, crearemos una API de película simple que nos permita realizar operaciones CRUD en instancias de “Película”.

Para comenzar, instalemos Flask si aún no lo tiene:

1
$ pip install flask

A continuación, necesitaremos acceso a una instancia de MongoDB, MongoDB proporciona una instancia en la nube, el [Mongo DB Atlas] (https://www.mongodb.com/cloud/atlas), que podemos usar de forma gratuita, sin embargo, usaremos una instancia instalada localmente. Las instrucciones para obtener e instalar MongoDB se pueden encontrar en la documentación oficial.

Y una vez hecho esto, también querremos instalar la biblioteca Flask-MongoEngine:

1
$ pip install flask-mongoengine

Conexión a una instancia de base de datos de MongoDB

Ahora que hemos instalado Flask y Flask-MongoEngine, necesitamos conectar nuestra aplicación Flask con una instancia de MongoDB.

Comenzaremos importando Flask y Flask-MongoEngine a nuestra aplicación:

1
2
from flask import Flask
from flask_mongoengine import MongoEngine

Luego, podemos crear el objeto de la aplicación Flask:

1
app = Flask(__name__)

Que usaremos para inicializar un objeto MongoEngine. Pero antes de que se complete la inicialización, necesitaremos una referencia a nuestra instancia de MongoDB.

Esta referencia es una clave en app.config cuyo valor es un dictado que contiene los parámetros de conexión:

1
2
3
4
5
app.config['MONGODB_SETTINGS'] = {
    'db':'db_name',
    'host':'localhost',
    'port':'27017'
}

También podríamos proporcionar un URI de conexión en su lugar:

1
2
3
app.config['MONGODB_SETTINGS'] = {
    'host':'mongodb://localhost/db_name'
}

Con la configuración hecha, ahora podemos inicializar un objeto MongoEngine:

1
db = MongoEngine(app)

También podríamos usar el método init_app() del objeto MongoEngine para la inicialización:

1
2
db = MongoEngine()
db.init_app(app)

Una vez que se hayan realizado la configuración y las inicializaciones, podemos comenzar a explorar algunas de las increíbles características de MongoEngine.

Creación de clases modelo

Al ser un ODM, MongoEngine usa clases de Python para representar documentos en nuestra base de datos.

MongoEngine proporciona varios tipos de clases de documentos:

  1. Documento
  2. Documento incrustado
  3. Documento dinámico
  4. Documento incrustado dinámico

Documento

Esto representa un documento que tiene su propia colección en la base de datos, se crea al heredar de mongoengine.Document o de nuestra instancia MongoEngine (db.Document):

1
2
3
4
5
6
7
8
class Movie(db.Document):
    title = db.StringField(required=True)
    year = db.IntField()
    rated = db.StringField()
    director = db.ReferenceField(Director)
    cast = db.EmbeddedDocumentListField(Cast)
    poster = db.FileField()
    imdb = db.EmbeddedDocumentField(Imdb)

MongoEngine también proporciona clases adicionales que describen y validan el tipo de datos que deben tomar los campos de un documento y modificadores opcionales para agregar más detalles o restricciones a cada campo.

Ejemplos de campos son:

  1. StringField() para valores de cadena
  2. IntField() para valores int
  3. ListField() para una lista
  4. FloatField() para valores de punto flotante
  5. ReferenceField() para hacer referencia a otros documentos
  6. EmbeddedDocumentField() para documentos incrustados, etc.
  7. FileField() para almacenar archivos (más sobre esto más adelante)

También puede aplicar modificadores en estos campos, como:

  • requerido
  • predeterminado
  • único
  • clave_principal etc.

Al establecer cualquiera de estos en Verdadero, se aplicarán específicamente a ese campo.

Documento incrustado

Esto representa un documento que no tiene su propia colección en la base de datos pero está incrustado en otro documento, se crea al heredar de la clase EmbeddedDocument:

1
2
3
4
class Imdb(db.EmbeddedDocument):
    imdb_id = db.StringField()
    rating = db.DecimalField()
    votes = db.IntField()

Documento dinámico

Este es un documento cuyos campos se agregan dinámicamente, aprovechando la naturaleza dinámica de MongoDB.

Al igual que los otros tipos de documentos, MongoEngine proporciona una clase para DynamicDocuments:

1
2
class Director(db.DynamicDocument):
    pass

Documento incrustado dinámico

Esto tiene todas las propiedades de DynamicDocument y EmbeddedDocument

1
2
class Cast(db.DynamicEmbeddedDocument):
    pass

Como hemos terminado de crear todas nuestras clases de datos, es hora de comenzar a explorar algunas de las características de MongoEngine.

Acceso a documentos {#acceso a documentos}

MongoEngine hace que sea muy fácil consultar nuestra base de datos, podemos obtener todas las películas en la base de datos así;

1
2
3
4
5
6
from flask import jsonify

@app.route('/movies')
def  get_movies():
    movies = Movie.objects()
    return  jsonify(movies), 200

Si enviamos una solicitud GET a:

1
localhost:5000/movies/

Esto devolverá todas las películas como una lista JSON:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[
 {
     "_id": {
         "$oid": "600eb604b076cdbc347e2b99"
         },
     "cast": [],
     "rated": "5",
     "title": "Movie 1",
     "year": 1998
 },
 {
     "_id": {
         "$oid": "600eb604b076cdbc347e2b9a"
         },
     "cast": [],
     "rated": "4",
     "title": "Movie 2",
     "year": 1999
 }
]

Cuando se trata de grandes resultados de consultas como estas, querrá truncarlos y permitir que el usuario final cargue lentamente más según sea necesario.

Flask-MongoEngine nos permite paginar los resultados muy fácilmente:

1
2
3
4
5
6
@app.route('/movies')
def get_movies():
    page = int(request.args.get('page',1))
    limit = int(request.args.get('limit',10))
    movies = Movie.objects.paginate(page=page, per_page=limit)
    return jsonify([movie.to_dict() for movie in movies.items]), 200

Movie.objects.paginate(page=page, per_page=limit) devuelve un objeto Pagination que contiene la lista de películas en su propiedad .items, iterando a través de la propiedad, obtenemos nuestras películas en la página seleccionada :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[
    {
        "_id": {
            "$oid": "600eb604b076cdbc347e2b99"
        },
        "cast": [],
        "rated": "5",
        "title": "Back to The Future III",
        "year": 1998
    },
    {
        "_id": {
            "$oid": "600fb95dcb1ba5529bbc69e8"
        },
        "cast": [],
        "rated": "4",
        "title": "Spider man",
        "year": 2004
    },
...
]

Obtener un documento

Podemos recuperar un único resultado de Película pasando el id como parámetro al método Movie.objects():

1
2
3
4
@app.route('/movies/<id>')
def get_one_movie(id: str):
    movie = Movie.objects(id=id).first()
    return jsonify(movie), 200

Movie.objects(id=id) devolverá un conjunto de todas las películas cuyo id coincide con el parámetro y first() devuelve el primer objeto Movie en el conjunto de consulta, si hay varios.

Si enviamos una solicitud GET a:

1
localhost:5000/movies/600eb604b076cdbc347e2b99

Obtendremos este resultado:

1
2
3
4
5
6
7
8
9
{
    "_id": {
        "$oid": "600eb604b076cdbc347e2b99"
    },
    "cast": [],
    "rated": "5",
    "title": "Back to The Future III",
    "year": 1998
}

Para la mayoría de los casos de uso, nos gustaría generar un error 404_NOT_FOUND si ningún documento coincide con el id proporcionado. Flask-MongoEngine nos tiene cubiertos con sus conjuntos de consultas personalizados first_or_404() y get_or_404():

1
2
3
4
@app.route('/movies/<id>')
def get_one_movie(id: str):
    movie = Movie.objects.first_or_404(id=id)
    return movie.to_dict(), 200

Crear/Guardar documentos

MongoEngine hace que sea muy fácil crear nuevos documentos usando nuestros modelos. Todo lo que tenemos que hacer es llamar al método save() en nuestra instancia de clase modelo como se muestra a continuación:

1
2
3
4
5
@app.route('/movies/', methods=["POST"])
def add_movie():
    body = request.get_json()
    movie = Movie(**body).save()
    return jsonify(movie), 201

**cuerpo descomprime el diccionario cuerpo en el objeto Película como parámetros con nombre. Por ejemplo, si cuerpo = {"título": "Título de la película", "año": 2015},
Entonces Película(**cuerpo) es lo mismo que Película(título="Título de la película", año=2015)

Si enviamos esta solicitud a localhost:5000/movies/:

1
2
3
$ curl -X POST -H "Content-Type: application/json" \
    -d '{"title": "Spider Man 3", "year": 2009, "rated": "5"}' \
    localhost:5000/movies/

Guardará y devolverá el documento:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "_id": {
    "$oid": "60290817f3918e990ba24f14"
  }, 
  "cast": [], 
  "director": {
    "$oid": "600fb8138724900858706a56"
  }, 
  "rated": "5", 
  "title": "Spider Man 3", 
  "year": 2009
}

Creación de documentos con documentos integrados {#creación de documentos con documentos integrados}

Para agregar un documento incrustado, primero debemos crear el documento para incrustarlo y luego asignarlo al campo apropiado en nuestro modelo de película:

1
2
3
4
5
6
7
8
@app.route('/movies-embed/', methods=["POST"])
def add_movie_embed():
    # Created Imdb object
    imdb = Imdb(imdb_id="12340mov", rating=4.2, votes=7.9)
    body = request.get_json()
    # Add object to movie and save
    movie = Movie(imdb=imdb, **body).save()
    return jsonify(movie), 201

Si enviamos esta solicitud:

1
2
3
$ curl -X POST -H "Content-Type: application/json"\
    -d '{"title": "Batman", "year": 2016, "rated": "yes"}'\
    localhost:5000/movies-embed/

Esto devolverá el documento recién agregado con el documento incrustado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
   "_id": {
       "$oid": "601096176cc65fa421dd905d"
   },
   "cast": [],
   "imdb": {
       "imdb_id": "12340mov",
       "rating": 4.2,
       "votes": 7
   },
   "rated": "yes",
   "title": "Batman",
   "year": 2016
}

Creación de documentos dinámicos

Como no se definieron campos en el modelo, necesitaremos proporcionar cualquier conjunto arbitrario de campos a nuestro objeto de documento dinámico.

Puede poner cualquier número de campos aquí, de cualquier tipo. Ni siquiera es necesario que los tipos de campo sean uniformes entre varios documentos.

Hay algunas maneras de lograr esto:

  • Podríamos crear el objeto del documento con todos los campos que queremos agregar como si fuera una solicitud como la que hemos hecho hasta ahora:

    1
    2
    3
    4
    5
    
    @app.route('/director/', methods=['POST'])
    def add_dir():
        body = request.get_json()
        director = Director(**body).save()
        return jsonify(director), 201
    
  • Podríamos crear el objeto primero, luego agregar los campos usando la notación de puntos y llamar al método de guardar cuando hayamos terminado:

    1
    2
    3
    4
    5
    6
    7
    8
    
    @app.route('/director/', methods=['POST'])
    def add_dir():
        body = request.get_json()
        director = Director()
        director.name = body.get("name")
        director.age = body.get("age")
        director.save()
        return jsonify(director), 201
    
  • Y finalmente, podríamos usar el método setattr() de Python:

    1
    2
    3
    4
    5
    6
    7
    8
    
    @app.route('/director/', methods=['POST'])
    def add_dir():
        body = request.get_json()
        director = Director()
        setattr(director, "name", body.get("name"))
        setattr(director, "age", body.get("age"))
        director.save()
        return jsonify(director), 201
    

En cualquier caso, podemos agregar cualquier conjunto de campos, ya que una implementación de DynamicDocument no define ninguno por sí misma.

Si enviamos una solicitud POST a localhost:5000/director/:

1
2
3
$ curl -X POST -H "Content-Type: application/json"\
    -d '{"name": "James Cameron", "age": 57}'\
    localhost:5000/director/

Esto resulta en:

1
2
3
4
5
6
7
{
  "_id": {
    "$oid": "6029111e184c2ceefe175dfe"
  }, 
  "age": 57, 
  "name": "James Cameron"
}

Actualización de documentos

Para actualizar un documento, recuperamos el documento persistente de la base de datos, actualizamos sus campos y llamamos al método update() en el objeto modificado en la memoria:

1
2
3
4
5
6
@app.route('/movies/<id>', methods=['PUT'])
def update_movie(id):
    body = request.get_json()
    movie = Movie.objects.get_or_404(id=id)
    movie.update(**body)
    return jsonify(str(movie.id)), 200

Enviemos una solicitud de actualización:

1
2
3
$ curl -X PUT -H "Content-Type: application/json"\
    -d '{"year": 2016}'\
    localhost:5000/movies/600eb609b076cdbc347e2b9a/

Esto devolverá la identificación del documento actualizado:

1
"600eb609b076cdbc347e2b9a"

También podríamos actualizar muchos documentos a la vez usando el método update(). Simplemente consultamos la base de datos para los documentos que pretendemos actualizar, dada alguna condición, y llamamos al método de actualización en el Queryset resultante:

1
2
3
4
5
6
@app.route('/movies_many/<title>', methods=['PUT'])
def update_movie_many(title):
    body = request.get_json()
    movies = Movie.objects(year=year)
    movies.update(**body)
    return jsonify([str(movie.id) for movie in movies]), 200

Enviemos una solicitud de actualización:

1
2
3
$ curl -X PUT -H "Content-Type: application/json"\
    -d '{"year": 2016}'\
    localhost:5000/movies_many/2010/

Esto devolverá una lista de ID de los documentos actualizados:

1
2
3
4
5
6
7
[
  "60123af478a2c347ab08c32b", 
  "60123b0989398f6965f859ab", 
  "60123bfe2a91e52ba5434630", 
  "602907f3f3918e990ba24f13", 
  "602919f67e80d573ad3f15e4"
]

Eliminación de documentos

Al igual que el método update(), el método delete() elimina un objeto, en función de su campo id:

1
2
3
4
5
@app.route('/movies/<id>', methods=['DELETE'])
def delete_movie(id):
    movie = Movie.objects.get_or_404(id=id)
    movie.delete()
    return jsonify(str(movie.id)), 200

Por supuesto, dado que es posible que no tengamos una garantía de que un objeto con la ID dada esté presente en la base de datos, usamos el método get_or_404() para recuperarlo, antes de llamar a delete().

Enviemos una solicitud de eliminación:

1
2
$ curl -X DELETE -H "Content-Type: application/json"\
    localhost:5000/movies/600eb609b076cdbc347e2b9a/

Esto resulta en:

1
"600eb609b076cdbc347e2b9a"

También podríamos eliminar muchos documentos a la vez, para hacer esto, consultaríamos en la base de datos los documentos que queremos eliminar y luego llamaríamos al método delete() en el Queryset resultante.

Por ejemplo, para eliminar todas las películas realizadas en un año determinado, haríamos algo como lo siguiente:

1
2
3
4
5
@app.route('/movies/delete-by-year/<year>/', methods=['DELETE'])
def delete_movie_by_year(year):
    movies = Movie.objects(year=year)
    movies.delete()
    return jsonify([str(movie.id) for movie in movies]), 200

Enviemos una solicitud de eliminación, eliminando todas las entradas de películas para el año 2009:

1
$ curl -X DELETE -H "Content-Type: application/json" localhost:5000/movies/delete-by-year/2009/

Esto resulta en:

1
2
3
4
5
[
  "60291fdd4756f7031638b703", 
  "60291fde4756f7031638b704", 
  "60291fdf4756f7031638b705"
]

Trabajar con archivos

Creación y almacenamiento de archivos

MongoEngine hace que sea muy fácil interactuar con MongoDB GridFS para almacenar y recuperar archivos. MongoEngine logra esto a través de su FileField().

Echemos un vistazo a cómo podemos cargar un archivo en MongoDB GridFS usando MongoEngine:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@app.route('/movies_with_poster', methods=['POST'])
def add_movie_with_image():
    # 1
    image = request.files['file']
    # 2
    movie = Movie(title = "movie with poster", year=2021)
    # 3
    movie.poster.put(image, filename=image.filename)
    # 4
    movie.save()
    # 5
    return jsonify(movie), 201

Repasemos el bloque anterior, línea por línea:

  1. Primero obtenemos una imagen de la clave file en request.files
  2. A continuación creamos un objeto Película
  3. A diferencia de otros campos, no podemos asignar un valor a FileField() usando el operador de asignación normal, sino que usaremos el método put() para enviar nuestra imagen. El método put() toma como argumentos el archivo que se cargará (debe ser un objeto similar a un archivo o un flujo de bytes), el nombre del archivo y los metadatos opcionales.
  4. Para guardar nuestro archivo, llamamos al método save() en el objeto de la película, como de costumbre.
  5. Devolvemos el objeto movie con un id que hace referencia a la imagen:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "_id": {
      "$oid": "60123e4d2628f541032a0900"
  },
  "cast": [],
  "poster": {
      "$oid": "60123e4d2628f541032a08fe"
  },
  "title": "movie with poster",
  "year": 2021
}

Como puede ver en la respuesta JSON, el archivo en realidad se guarda como un documento MongoDB separado, y solo tenemos una referencia de base de datos.

Recuperando archivos {#recuperando archivos}

Una vez que hemos puesto() un archivo en FileField(), podemos leer() de nuevo en la memoria, una vez que tenemos un objeto que contiene ese campo. Echemos un vistazo a cómo podemos recuperar archivos de documentos MongoDB:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from io import BytesIO 
from flask.helpers import send_file

@app.route('/movies_with_poster/<id>/', methods=['GET'])
def get_movie_image(id):
    
    # 1
    movie = Movie.objects.get_or_404(id=id)
    # 2
    image = movie.poster.read()
    content_type = movie.poster.content_type
    filename = movie.poster.filename
    # 3
    return send_file(
        # 4
        BytesIO(image), 
        attachment_filename=filename, 
        mimetype=content_type), 200

Echemos un vistazo a lo que se hace en los segmentos:

  1. Recuperamos el documento de la película que contiene una imagen.
  2. Luego guardamos la imagen como una cadena de bytes en la variable image, obtuvimos el nombre de archivo y el tipo de contenido y los guardamos en las variables filename y content_type.
  3. Usando el método auxiliar send_file() de Flask, intentamos enviar el archivo al usuario, pero dado que la imagen es un objeto bytes, obtendríamos un AttributeError: el objeto 'bytes' no tiene El atributo 'read' como send_file() espera un objeto similar a un archivo, no bytes.
  4. Para resolver este problema, usamos la clase BytesIO() del módulo io para decodificar el objeto de bytes en un objeto similar a un archivo que send_file() puede enviar.

Eliminación de archivos

Eliminar documentos que contienen archivos no eliminará el archivo de GridFS, ya que se almacenan como objetos separados.

Para eliminar los documentos y los archivos que los acompañan, primero debemos eliminar el archivo antes de eliminar el documento.

FileField() también proporciona un método delete() que podemos usar para simplemente eliminarlo de la base de datos y del sistema de archivos, antes de continuar con la eliminación del objeto en sí:

1
2
3
4
5
6
@app.route('/movies_with_poster/<id>/', methods=['DELETE'])
def delete_movie_image(id):
    movie = Movie.objects.get_or_404(id=id)
    movie.poster.delete()
    movie.delete()
    return "", 204

Conclusión

MongoEngine proporciona una interfaz Pythonic relativamente simple pero rica en funciones para interactuar con MongoDB desde una aplicación python y Flask-MongoEngine facilita aún más la integración de MongoDB en nuestras aplicaciones Flask.

En esta guía, hemos explorado algunas de las características de MongoEngine y su extensión Flask. Creamos una API CRUD simple y usamos MongoDB GridFS para guardar, recuperar y eliminar archivos usando MongoEngine. En esta guía, hemos explorado algunas de las características de MongoEngine y su extensión Flask. Creamos una API CRUD simple y usamos MongoDB GridFS para guardar, recuperar y eliminar archivos usando MongoEngine.