Вступление
Создание веб-приложения почти всегда означает работу с данными из базы данных. Существуют различные базы данных на выбор, в зависимости от ваших предпочтений.
В этом руководстве мы рассмотрим, как интегрировать одну из самых популярных баз данных 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 предоставляет несколько типов классов документов:
- Документ
- EmbeddedDocument
- DynamicDocument
- 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 также предоставляет дополнительные классы, которые описывают и проверяют тип данных, которые должны принимать поля документа, и необязательные модификаторы для добавления дополнительных деталей или ограничений к каждому полю.
Примеры полей:
StringField()
для строковых значенийIntField()
для значений типа intListField()
для спискаFloatField()
для значений с плавающей запятойReferenceField()
для ссылки на другие документыEmbeddedDocumentField()
для встроенных документов и т. Д.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
Давайте рассмотрим приведенный выше блок построчно:
- Сначала мы получаем изображение из ключевого
file
вrequest.files
- Затем мы создаем объект
Movie
- В отличие от других полей, мы не можем присвоить значение
FileField()
с помощью обычного оператора присваивания, вместо этого мы будем использовать методput()
для отправки нашего изображения. Методput()
принимает в качестве аргументов файл, который нужно загрузить (это должен быть файловый объект или поток байтов), имя файла и необязательные метаданные. - Чтобы сохранить наш файл, мы
save()
для объекта фильма. - Мы возвращаем
movie
с идентификатором, ссылающимся на изображение:
|
|
{
"_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
Давайте посмотрим, что делается в сегментах:
- Мы получили документ фильма, содержащий изображение.
- Затем мы сохранили изображение как строку байтов в
image
, получили имя файла и тип содержимого и сохранили их в переменныхfilename
иcontent_type
- Используя
send_file()
, мы пытаемся отправить файл пользователю, но поскольку изображение являетсяbytes
объектом, мы получимAttributeError: 'bytes' object has no attribute 'read'
посколькуsend_file()
ожидает файловый объект, а не байты. - Чтобы решить эту проблему, мы используем
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.