Руководство по Flask-MongoEngine в Python

Введение Создание веб-приложения почти всегда означает работу с данными из базы данных. Существуют различные базы данных на выбор, в зависимости от ваших предпочтений. В этом руководстве мы рассмотрим, как интегрировать одну из самых популярных баз данных NoSQL - MongoDB - с микро-фреймворком Flask. В этом руководстве мы рассмотрим, как интегрировать MongoDB с Flask, используя популярную библиотеку - MongoEngine [http://mongoengine.org/#home], а точнее ее оболочку - Flask-MongoEng.

Вступление

Создание веб-приложения почти всегда означает работу с данными из базы данных. Существуют различные базы данных на выбор, в зависимости от ваших предпочтений.

В этом руководстве мы рассмотрим, как интегрировать одну из самых популярных баз данных NoSQL - MongoDB - с микро-фреймворком Flask.

В этом руководстве мы рассмотрим, как интегрировать MongoDB с Flask, используя популярную библиотеку - MongoEngine , а точнее ее оболочку

  • Flask-MongoEngine .

Кроме того, вы можете интегрировать MongoDB с Flask-PyMongo .

Flask-MongoEngine

MongoEngine - это ODM (Object Document Mapper), который сопоставляет классы (модели) Python с документами MongoDB, что упрощает создание документов и управление ими программно прямо из нашего кода.

Установка и конфигурация

Чтобы изучить некоторые функции MongoEngine, мы создадим простой API-интерфейс для фильмов, который позволит нам выполнять операции CRUD с экземплярами Movie

Для начала давайте установим Flask, если у вас его еще нет:

 $ pip install flask 

Далее нам понадобится доступ к экземпляру MongoDB, MongoDB предоставляет экземпляр облака - Атлас MongoDB, который мы можем использовать бесплатно, однако мы будем использовать локально установленный экземпляр. Инструкции по получению и установке MongoDB можно найти в официальной документации .

И после этого мы также захотим установить библиотеку Flask-MongoEngine:

 $ pip install flask-mongoengine 

Подключение к экземпляру базы данных MongoDB

Теперь, когда мы установили Flask и Flask-MongoEngine, нам нужно подключить наше приложение Flask к экземпляру MongoDB.

Начнем с импорта Flask и Flask-MongoEngine в наше приложение:

 from flask import Flask 
 from flask_mongoengine import MongoEngine 

Затем мы можем создать объект приложения Flask:

 app = Flask(__name__) 

Который мы будем использовать для инициализации объекта MongoEngine Но до завершения инициализации нам понадобится ссылка на наш экземпляр MongoDB.

Эта ссылка является ключом в app.config , значением которого является dict, содержащий параметры подключения:

 app.config['MONGODB_SETTINGS'] = { 
 'db':'db_name', 
 'host':'localhost', 
 'port':'27017' 
 } 

Мы также могли бы вместо этого предоставить URI подключения:

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

После завершения настройки мы можем инициализировать объект MongoEngine

 db = MongoEngine(app) 

Мы также можем использовать метод init_app() MongoEngine для инициализации:

 db = MongoEngine() 
 db.init_app(app) 

После завершения настройки и инициализации мы можем приступить к изучению некоторых удивительных функций MongoEngine.

Создание классов модели

Будучи ODM, MongoEngine использует классы Python для представления документов в нашей базе данных.

MongoEngine предоставляет несколько типов классов документов:

  1. Документ
  2. EmbeddedDocument
  3. DynamicDocument
  4. DynamicEmbeddedDocument

Документ

Это представляет собой документ , который имеет свою собственную коллекцию в базе данных, она создается путем наследования от mongoengine.Document или из нашего MongoEngine экземпляра ( db.Document ):

 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 также предоставляет дополнительные классы, которые описывают и проверяют тип данных, которые должны принимать поля документа, и необязательные модификаторы для добавления дополнительных деталей или ограничений к каждому полю.

Примеры полей:

  1. StringField() для строковых значений
  2. IntField() для значений типа int
  3. ListField() для списка
  4. FloatField() для значений с плавающей запятой
  5. ReferenceField() для ссылки на другие документы
  6. EmbeddedDocumentField() для встроенных документов и т. Д.
  7. FileField() для хранения файлов (подробнее об этом позже)

Вы также можете применить к этим полям модификаторы, например:

  • required
  • default
  • unique
  • primary_key и т. д.

Если установить для любого из них значение True , они будут применяться конкретно к этому полю.

EmbeddedDocument

Это представляет собой документ, который не имеет собственной коллекции в базе данных, но встроен в другой документ, он создается путем наследования от класса EmbeddedDocument

 class Imdb(db.EmbeddedDocument): 
 imdb_id = db.StringField() 
 rating = db.DecimalField() 
 votes = db.IntField() 

DynamicDocument

Это документ, поля которого добавляются динамически, используя динамический характер MongoDB.

Как и другие типы документов, MongoEngine предоставляет класс для DynamicDocument s:

 class Director(db.DynamicDocument): 
 pass 

DynamicEmbeddedDocument

У него есть все свойства DynamicDocument и EmbeddedDocument

 class Cast(db.DynamicEmbeddedDocument): 
 pass 

Когда мы закончили создание всех наших классов данных, пришло время начать изучение некоторых функций MongoEngine.

Доступ к документам

MongoEngine позволяет очень легко запрашивать нашу базу данных, мы можем получить все фильмы в базе данных следующим образом;

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

Если мы отправим запрос GET на:

 localhost:5000/movies/ 

Это вернет все фильмы в виде списка JSON:

 [ 
 { 
 "_id": { 
 "$oid": "600eb604b076cdbc347e2b99" 
 }, 
 "cast": [], 
 "rated": "5", 
 "title": "Movie 1", 
 "year": 1998 
 }, 
 { 
 "_id": { 
 "$oid": "600eb604b076cdbc347e2b9a" 
 }, 
 "cast": [], 
 "rated": "4", 
 "title": "Movie 2", 
 "year": 1999 
 } 
 ] 

Когда вы имеете дело с большими результатами по таким запросам, вам нужно усечь их и позволить конечному пользователю медленно загружать больше по мере необходимости.

Flask-MongoEngine позволяет нам очень легко разбивать результаты на страницы:

 @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) возвращает Pagination объект, содержащий список фильмов , в его .items собственности, перебирая собственности, мы получаем наши фильмы на выбранной странице:

 [ 
 { 
 "_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 
 }, 
 ... 
 ] 

Получение одного документа

Мы можем получить один Movie , передав идентификатор в качестве параметра Movie.objects() :

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

Movie.objects(id=id) вернет набор всех фильмов, id соответствует параметру, а first() вернет первый Movie в наборе запросов, если их несколько.

Если мы отправим запрос GET на:

 localhost:5000/movies/600eb604b076cdbc347e2b99 

Получим такой результат:

 { 
 "_id": { 
 "$oid": "600eb604b076cdbc347e2b99" 
 }, 
 "cast": [], 
 "rated": "5", 
 "title": "Back to The Future III", 
 "year": 1998 
 } 

Для большинства случаев использования мы хотели бы вызвать 404_NOT_FOUND если ни один документ не соответствует предоставленному id . Flask-MongoEngine предоставил нам свои собственные first_or_404() и get_or_404() :

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

Создание / сохранение документов

MongoEngine упрощает создание новых документов с использованием наших моделей. Все, что нам нужно сделать, это вызвать метод save() в нашем экземпляре класса модели, как показано ниже:

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

**body распаковывает словарь body Movie как именованные параметры. Например, если body = {"title": "Movie Title", "year": 2015} ,
Movie(**body) совпадает с Фильмом Movie(title="Movie Title", year=2015)

Если мы отправим этот запрос на localhost:5000/movies/ :

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

Он сохранит и вернет документ:

 { 
 "_id": { 
 "$oid": "60290817f3918e990ba24f14" 
 }, 
 "cast": [], 
 "director": { 
 "$oid": "600fb8138724900858706a56" 
 }, 
 "rated": "5", 
 "title": "Spider Man 3", 
 "year": 2009 
 } 

Создание документов со встроенными документами

Чтобы добавить внедренный документ, нам сначала нужно создать документ для встраивания, а затем назначить его соответствующему полю в нашей модели фильма:

 @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 

Если мы отправим этот запрос:

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

Это вернет недавно добавленный документ со встроенным документом:

 { 
 "_id": { 
 "$oid": "601096176cc65fa421dd905d" 
 }, 
 "cast": [], 
 "imdb": { 
 "imdb_id": "12340mov", 
 "rating": 4.2, 
 "votes": 7 
 }, 
 "rated": "yes", 
 "title": "Batman", 
 "year": 2016 
 } 

Создание динамических документов

Поскольку в модели не определены поля, нам нужно будет предоставить произвольный набор полей нашему объекту динамического документа.

Здесь вы можете ввести любое количество полей любого типа. Вам даже не нужно, чтобы типы полей были одинаковыми для нескольких документов.

Есть несколько способов добиться этого:

  • Мы могли бы создать объект документа со всеми полями, которые мы хотим добавить, как если бы запрос, как мы это делали до сих пор:

     @app.route('/director/', methods=['POST']) 
     def add_dir(): 
     body = request.get_json() 
     director = Director(**body).save() 
     return jsonify(director), 201 
    
  • Мы могли бы сначала создать объект, затем добавить поля, используя точечную нотацию, и вызвать метод сохранения, когда мы закончим:

     @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 
    
  • И, наконец, мы можем использовать метод Python setattr() :

     @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 
    

В любом случае мы можем добавить любой набор полей, поскольку DynamicDocument сама себя не определяет.

Если мы отправим запрос POST на localhost:5000/director/ :

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

Это приводит к:

 { 
 "_id": { 
 "$oid": "6029111e184c2ceefe175dfe" 
 }, 
 "age": 57, 
 "name": "James Cameron" 
 } 

Обновление документов

Чтобы обновить документ, мы извлекаем постоянный документ из базы данных, обновляем его поля и вызываем метод update() для измененного объекта в памяти:

 @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 

Отправим запрос на обновление:

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

Это вернет идентификатор обновленного документа:

 "600eb609b076cdbc347e2b9a" 

Мы также можем обновить сразу несколько документов, используя метод update() . Мы просто запрашиваем в базе данных документы, которые мы собираемся обновить, при определенных условиях и вызываем метод обновления для полученного Queryset:

 @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 

Отправим запрос на обновление:

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

Это вернет список идентификаторов обновленных документов:

 [ 
 "60123af478a2c347ab08c32b", 
 "60123b0989398f6965f859ab", 
 "60123bfe2a91e52ba5434630", 
 "602907f3f3918e990ba24f13", 
 "602919f67e80d573ad3f15e4" 
 ] 

Удаление документов

Подобно методу update() delete() удаляет объект на основе его поля id

 @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 

Конечно, поскольку у нас может не быть гарантии, что объект с данным идентификатором присутствует в базе данных, мы используем метод get_or_404() для его получения перед вызовом delete() .

Отправим запрос на удаление:

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

Это приводит к:

 "600eb609b076cdbc347e2b9a" 

Мы также можем удалить сразу несколько документов, для этого мы будем запрашивать в базе данных документы, которые хотим удалить, а затем вызывать метод delete() для полученного Queryset.

Например, чтобы удалить все фильмы, снятые в определенном году, мы должны сделать что-то вроде следующего:

 @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 

Отправим запрос на удаление, удалив все записи фильмов за 2009 :

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

Это приводит к:

 [ 
 "60291fdd4756f7031638b703", 
 "60291fde4756f7031638b704", 
 "60291fdf4756f7031638b705" 
 ] 

Работа с файлами

Создание и хранение файлов

MongoEngine упрощает взаимодействие с MongoDB GridFS для хранения и извлечения файлов. MongoEngine достигает этого с помощью своего FileField() .

Давайте посмотрим, как мы можем загрузить файл в MongoDB GridFS с помощью MongoEngine:

 @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 

Давайте рассмотрим приведенный выше блок построчно:

  1. Сначала мы получаем изображение из ключевого file в request.files
  2. Затем мы создаем объект Movie
  3. В отличие от других полей, мы не можем присвоить значение FileField() с помощью обычного оператора присваивания, вместо этого мы будем использовать метод put() для отправки нашего изображения. Метод put() принимает в качестве аргументов файл, который нужно загрузить (это должен быть файловый объект или поток байтов), имя файла и необязательные метаданные.
  4. Чтобы сохранить наш файл, мы save() для объекта фильма.
  5. Мы возвращаем movie с идентификатором, ссылающимся на изображение:
1
<!-- -->
 { 
 "_id": { 
 "$oid": "60123e4d2628f541032a0900" 
 }, 
 "cast": [], 
 "poster": { 
 "$oid": "60123e4d2628f541032a08fe" 
 }, 
 "title": "movie with poster", 
 "year": 2021 
 } 

Как видно из ответа JSON, файл на самом деле сохраняется как отдельный документ MongoDB, и у нас просто есть ссылка на него в базе данных.

Получение файлов

После того, как мы put() файл в FileField() , мы можем read() его обратно в память, как только у нас будет объект, содержащий это поле. Давайте посмотрим, как мы можем извлекать файлы из документов MongoDB:

 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 

Давайте посмотрим, что делается в сегментах:

  1. Мы получили документ фильма, содержащий изображение.
  2. Затем мы сохранили изображение как строку байтов в image , получили имя файла и тип содержимого и сохранили их в переменных filename и content_type
  3. Используя send_file() , мы пытаемся отправить файл пользователю, но поскольку изображение является bytes объектом, мы получим AttributeError: 'bytes' object has no attribute 'read' поскольку send_file() ожидает файловый объект, а не байты.
  4. Чтобы решить эту проблему, мы используем BytesIO() из io чтобы декодировать объект bytes обратно в объект, send_file() может отправлять send_file ().

Удаление файлов

Удаление документов, содержащих файлы, не приведет к удалению файла из GridFS, поскольку они хранятся как отдельные объекты.

Чтобы удалить документы и сопровождающие их файлы, мы должны сначала удалить файл перед удалением документа.

FileField() также предоставляет delete() который мы можем использовать, чтобы просто удалить его из базы данных и файловой системы, прежде чем мы продолжим удаление самого объекта:

 @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 

Заключение

MongoEngine предоставляет относительно простой, но многофункциональный интерфейс Pythonic для взаимодействия с MongoDB из приложения Python, а Flask-MongoEngine упрощает интеграцию MongoDB в наши приложения Flask.

В этом руководстве мы изучили некоторые особенности MongoEngine и его расширения Flask. Мы создали простой CRUD API и использовали MongoDB GridFS для сохранения, извлечения и удаления файлов с помощью MongoEngine. В этом руководстве мы исследовали некоторые особенности MongoEngine и его расширения Flask. Мы создали простой CRUD API и использовали MongoDB GridFS для сохранения, извлечения и удаления файлов с помощью MongoEngine.

comments powered by Disqus