Работа с базой данных во Flask: от джуна джуну

Написать данную статью меня побудило желание помочь таким же новичкам в Python в целом и в работе с Flask в частности, как я сам. Во время работы над задачей целостного и понятного объяснения в том стиле, как любим мы, новички, не нашел. Приходилось информацию искать по крупицам. Каких-то картинок не будет. Сугубо техническая статья. Опытным людям буду благодарен за комментарии и за подсказки по улучшению кода.

Итак, приступим.

Я начал изучать Python сразу после Нового года. По истечению четырех месяцев я понял, что теория, а также обучение на вымышленных задачах, которые не уходят в прод, особо не научишься, решил искать «боевые» задачи. Для этого спрашивал у своих знакомых, есть ли у них какие-то реальные задачи, которые нужно запрограммировать. Один «мой знакомый друг» попросил реализовать бота для Телеграма (упущу суть бота; это не важно — мне нужно было взять любую задачу, у которого есть конкретный заказчик и реализовать так, как хочет заказчик).

В поисках решения естественно, что первое с чего начал — это известные фреймворки Python Telegram Bot и pyTelegramBotAPI. Сначала как для новичка мне это казалось просто находкой — можно было особо не разбираясь с нюансами кода быстро начать «пилить» реального бота. За пару дней я уперся в то, что я не могу создать нужный мне функционал. Вроде делал все по документации, но оно не решалось. Тогда я понял, что совсем не понимаю, как фреймворк работает «под капотом» и почему у меня не работает то, что вроде должно работать; где и какие команды нужно вызывать и с каким методами. В общем я решил отложить в сторону фреймворк и попробовать разобраться более глубже, как работает само API Telegram и как можно работать с ним напрямую, что даст мне больше контроля над ситуацией, а также позволит изучить внимательнее всю кухню работы с API. Вполне возможно, что я уже не вернусь к использованию фреймворков Python Telegram Bot и pyTelegramBotAPI за ненадобностью. А может и наоборот вернусь, чтобы упростить себе работу по созданию своего велосипеда работы с API Telegram. Но даже если вернусь, то буду намного глубже понимать работу этих фреймворков.

У меня лежал недосмотренным небольшой курс на Udemy как раз по созданию бота для Телеграма. На момент написания статьи это был единственный курс на Udemy, где человек решал задачу без Python Telegram Bot и pyTelegramBotAPI (давать ссылку не буду, чтобы это не было рекламой). Для решения он использовал Flask. Тут, кстати, после прохождения некотороего «боевого пути» у меня появилась записать свой курс на эту тему, хотя, конечно, рановато — особой ценности дополнительной не принесу. Но если ты, более опытный программист, читающий данную статью, знаешь многое об этом, то можешь создать свой курс и я у тебя его с удовольствием куплю за $10.99 (типовая «скидочная» цена на Udemy), чтобы узнать что-то новое.

В общем, из курса я понял, что Flask позволит мне облегчить жизнь для обработки GET- и POST-запросов.

Так как данная статья посвящена конкретно работе с БД, то буду рассказывать об этом. Хотя, все время подмывает описать и другие тонкости, такие как: вынесение настроек подключения в «секретный» файл, получение и обработка получаемых от API Telegram данных, как на локальном компьютере без ssl-сертификата получать webhooks от Telegram. Если у вас будет интерес, то дайте знать и я напишу отдельную статью.

Где-то дня 3-4 своего джуновского времени ушло у меня на то, чтобы понять, что мне нужно отойти от фреймворка, до того, как я смог уже уверенно получать данные от Телеграма, обрабатывать текст (парсить) и в зависимости от того, что написал пользователь, отправлять ему необходимые команды, а также кнопки (в том числе и перезаписывать сообщение и изменять кнопки).

Следующим этапом для меня было подключение БД, чтобы хранить в ней пользователей, которые работают с ботом, а также данные по их взаимодействию в ботом в рамках той задачи, которая стояла перед ним.

Первым делом в Гугле мне выпала статья с Хабра с переводом мега-учебника Miguel Grinberg (оригинал его блога здесь).

В четвертой главе как раз речь идет о подключении к БД с помощью Flask и ORM SQLAlchemy. Тогда я подумал: «Ух ты, как круто, теперь проблема решена». Как ни странно, но стуктура файлов, которую предложил автор, у меня не сработала.

Я сделал по аналогии как у него:

microblog\
  venv\
  app\
    __init__.py
    models.py
  main.py

В main.py у меня ведется вся основная работа с Telegram API.
В app\models.py я создаю классы для БД.
В app\__init__.py, все сделал, как написал Miguel Grinberg.

Но почему-то в main.py у меня не захотели подтягиваться from app import db, app. Потратил около часа на поиски проблемы, решения в интернете. В итоге наткнулся на канал на YouTube Олега Молчанов и его видео «Cоздание блога на Flask (уроки) — Cоздание постов (модели) и SQLAlchemy». Там подсмотрел как он делает подключение к БД и попробовал пойти по этому пути (без вынесения файла models.py в директорию app, без создания __init__.py.

В общем сейчас структура моего проекта проста до безобразия (и немного некрасиво, что меня смущает, может в будущем пойму, как улучшить структуру):

image

Как видите, у меня есть app.py, models.py, main.py, app.sqlite (остальные файлы не касаются текущей темы).

В app.py у меня такой код:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config

app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)

В models.py:

from datetime import datetime
from app import db

class Users(db.Model):
    # Создаем таблицу пользователей
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(120), index=True, unique=True)
    last_name = db.Column(db.String(128))
    first_name = db.Column(db.String(128))
    created = db.Column(db.DateTime, default=datetime.now())
    tasks = db.relationship('Tasks', backref='tasks')

    # Загоним данные в БД транзитом
    # def __init__(self, *args, **kwargs):
    #     super(Users, self).__init__(*args, **kwargs)

    def __init__(self, username, last_name, first_name):
        self.username = username
        self.last_name = last_name
        self.first_name = first_name

    def __repr__(self):
        return '<User {}>'.format(self.username)


class Tasks(db.Model):
    # Создаем таблицу задач пользователей
    __tablename__ = 'tasks'
    id = db.Column(db.Integer, primary_key=True)
    owner_id = db.Column(db.Integer(), db.ForeignKey('users.id'))
    name = db.Column(db.String(120), index=True)
    start = db.Column(db.Boolean, default=False)
    finish = db.Column(db.Boolean, default=False)
    created_on = db.Column(db.DateTime, default=datetime.now())
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)

    # Загоним данные в БД транзитом
    def __init__(self, *args, **kwargs):
        super(Tasks, self).__init__(*args, **kwargs)

    # def __init__(self, name, last_name, first_name):
    #     self.name = name
    #     self.last_name = last_name
    #     self.first_name = first_name

    def __repr__(self):
        return '<Tasks {}>'.format(self.name)

В config.py:

import os
from dotenv import load_dotenv
load_dotenv()

basedir = os.path.abspath(os.path.dirname(__file__))


# Соединяемся с базой данных
class Config(object):
    DEBUG = True
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
    # SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://{user}:{pw}@{url}/{db}'.format(user=os.environ.get('POSTGRES_USER'),
    #                                                                                 pw=os.environ.get('POSTGRES_PW'),
    #                                                                                 url=os.environ.get('POSTGRES_URL'),
    #
    #                                                                                 db=os.environ.get('POSTGRES_DB'))
    SQLALCHEMY_DATABASE_URI = (os.environ.get('DATABASE_URL') or
                               'sqlite:///' + os.path.join(basedir, 'app.sqlite')) + '?check_same_thread=False'
    SQLALCHEMY_TRACK_MODIFICATIONS = False  # silence the deprecation warning

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

Сначала в качестве БД у меня в SQLALCHEMY_DATABASE_URI была записана app.db (где сейчас app.sqlite). Да-да, так и записал, как в инструкции указал Miguel Grinberg. Надеюсь, вы понимаете, что тут нужно заменить на .sqlite. До этого я додумался через час после мучительных попыток запустить код.

Создать БД и таблицы в sqlite можно с зайдя в отладочную консоль (например, у меня в PyCharm это Tools -> Python or Debug Console:

image

Здесь сначала подключаем нужный нам метод в нужном файле (у меня это from models import db), а после успешного подключения указать команду db.create_all(). После этого будет создана БД со всеми нужными таблицами. При этом стоит знать, что если я сделаю from app import db и запущу команду db.create_all(), то файл БД вроде как создастся, но это будет какая-то ерунда, а не БД (не понял почему).

Когда я решил эту проблему, то снова подумал, что теперь уже никаких сложностей не осталось. Осталось только написать функцию для main.py, которая при определенных событиях будет записывать данные из Телеграма в БД. Ну, подключил from models import Users и в нужном месте вызывал функцию:

try:
    find_user_in_db(username, first_name, last_name)
except NameError:
    print("user не отработал")
except:
    print("An exception occurred")

def add_users_to_db(username, last_name, first_name):
    """
    Функция добавления пользователя в БД
    :return:
    """
    data = Users(username, last_name, first_name)
    db.session.add(data)
    db.session.commit()

def find_user_in_db(username, first_name, last_name):
    """
    Функция для проверки наличия пользователя в базе и при отсутствии оного добавление
    :param username:
    :param first_name:
    :param last_name:
    :return:
    """
    user = db.session.query(Users).filter(Users.username == f'{username}').all()
    if not user:
        add_users_to_db(username, first_name, last_name)

В последней функции сначала вместо user = db.session.query(Users).filter(Users.username == f'{username}').first_or_404() стояло Users.query.filter_by(username = '{username}').first_or_404(). Тут я тоже потратил около получаса, чтобы понять, что данный запрос в БД не работает и не получает никаких данных, поэтому и запрос к БД не отправляется. Гугл подсказал одну статейку, в котором люди сказали, что лучше использовать db.session.query(Users). Почему так, я хз, не стал тратить время на разборы. После этого данные начали записываться в БД.

На этом данную статью завершаю так как описал то, что хотел описать и решил проблему подключения БД к Flask.

Данная статья написана только по той причине, что мы «джуны» любим, когда читать инструкции. Сам я тоже пробовал найти инструкцию по подключению БД к Flask. Никакой полноценной инструкции не нашел, поэтому пришлось пробираться через преграды. Ну и решил описать свой опыт следующим, кто будет искать готовую инструкцию.

P.S. Кто ищет прямо готовое решение, то прошу в репозиторий на GitHub

P.S.S. Буду рад, если кто-то сделает ревью кода и даст свои рекомендации по улучшению.

Спасибо за внимание.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 3

    0
    А зачем еще одна статья из серии 101: как выйти из Vim?
      0
      Создать БД и таблицы в sqlite можно с зайдя в отладочную консоль

      Вы совсем не пользуетесь миграциями? Там тремя командами в терминале всё создается, в виртуальном окружении.
      Для обучения также можно порекомендовать книгу М.Гринберга «Flask Web Development, 2nd Edition». Там есть некоторые вещи, которые не пересекаются с тем, что написано в известном блоге. И наоборот, в книге нет некоторых моментов, которые описаны в блоге.
        0
        Здесь (это ссылка на хабра-перевод) понятный тьюториал про БД во Flask, а у вас, простите, какая-то жизнь-боль-слёзы «как мне джуну плохо от недосмотренных видеокурсов».

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

        Самое читаемое