Рубрики
Без рубрики

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

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

Автор оригинала: Geoffery, Joseph.

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

Вступление

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

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

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

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

Колба-Монгоинжин

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

Настройка и настройка

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

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

$ pip install flask

Далее нам понадобится доступ к экземпляру MongoDB, MongoDB предоставляет облачный экземпляр – MongoDB Atlas – который мы можем использовать бесплатно, однако мы будем использовать локально установленный экземпляр. Инструкции по получению и установке 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. Встроенный документ
  3. Динамический документ
  4. DynamicEmbeddedDocument

Документ

Это представляет собой документ, который имеет свою собственную коллекцию в базе данных, он создается путем наследования от mongoengine.Документ или из нашего 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. String Field() для строковых значений
  2. Int Field() для значений int
  3. List Field() для списка
  4. FloatField() для значений с плавающей запятой
  5. Справочное поле() для ссылок на другие документы
  6. EmbeddedDocumentField() для встроенных документов и т. Д.
  7. File Field() для хранения файлов (подробнее об этом позже)

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

  • требуемый
  • по умолчанию
  • уникальный
  • primary_key и т. Д.

Установив любой из них в True , они будут применены именно к этому полю.

Встроенный документ

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

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

Динамический документ

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

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

class Director(db.DynamicDocument):
    pass

DynamicEmbeddedDocument

Это имеет все свойства Динамического документа и Встроенного документа

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,) возвращает объект 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/')
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/')
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",)

Если мы отправим этот запрос на 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
}

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

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

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

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

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

Если мы отправим запрос 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/', 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 () . Мы просто запрашиваем базу данных для документов, которые мы намерены обновить, учитывая некоторые условия, и вызываем метод update для результирующего набора запросов:

@app.route('/movies_many/', 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/', 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() в результирующем наборе запросов.

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

@app.route('/movies/delete-by-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 in request.файлы
  2. Далее мы создаем объект Movie
  3. В отличие от других полей, мы не можем присвоить значение полю File() с помощью обычного оператора присваивания, вместо этого мы будем использовать метод put() для отправки нашего изображения. Метод put() принимает в качестве аргументов загружаемый файл (это должен быть файлоподобный объект или байтовый поток), имя файла и необязательные метаданные.
  4. Чтобы сохранить наш файл, мы, как обычно, вызываем метод save() для объекта movie.
  5. Мы возвращаем объект movie с идентификатором, ссылающимся на изображение:
{
  "_id": {
      "$oid": "60123e4d2628f541032a0900"
  },
  "cast": [],
  "poster": {
      "$oid": "60123e4d2628f541032a08fe"
  },
  "title": "movie with poster",
  "year": 2021
}

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

Извлечение файлов

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

from io import BytesIO 
from flask.helpers import send_file

@app.route('/movies_with_poster//', 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. Используя вспомогательный метод Flasks send_file () , мы пытаемся отправить файл пользователю, но так как изображение является объектом bytes , мы получим AttributeError: объект 'bytes' не имеет атрибута 'read' as Чтобы решить эту проблему, мы используем класс BytesIO()
  4. из модуля io для декодирования объекта bytes обратно в файлоподобный объект, который может отправить send_file () .

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

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

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

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

@app.route('/movies_with_poster//', 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 и его расширения колбы. Мы создали простой CRUD API и использовали MongoDB GridFS для сохранения, извлечения и удаления файлов с помощью MongoEngine.В этом руководстве мы изучили некоторые особенности MongoEngine и его расширения колбы. Мы создали простой CRUD API и использовали MongoDB GridFS для сохранения, извлечения и удаления файлов с помощью MongoEngine.