Для дальнейшей реализации логики бота мне необходимо проработать вопросы, связанные с хранением и обработкой данных.
Первые хендлеры, обрабатывающие команды /start, /help, нажатие кнопок «Готов!», «Справка по боту» и кнопки «Начать ввод данных» не требовали какой-то сложной логики и данных. Все тексты я брал из ранее созданного словаря, и этого было достаточно.
Однако далее мне необходимо показать пользователю последовательно две inline клавиатуры. Первая клавиатура будет представлять из себя список кнопок с названиями федеральных округов. Вторая клавиатура – это список с названиями регионов в том федеральном округе, который выбрал пользователь.
Для первой клавиатуры данные я буду брать из уже готового словаря, правда, я его немного переделал по сравнению с примером, который показывал в предыдущем посте.
Образец словаря в предыдущей редакции:
'federal_districts': { 30: 'Центральный федеральный округ', 31: 'Северо-Западный федеральный округ', ... }
Новая редакция словаря:
'federal_districts': { 'Центральный федеральный округ': 30, 'Северо-Западный федеральный округ': 31, ... }
А вот данные для текста и колбэков inline кнопок для клавиатур с регионами в выбранном пользователем федеральном округе я буду брать из таблицы в базе данных. Но пока у меня нет ни таблицы, ни самой базы данных. Надо это исправлять.
ORM Peewee, модель Region
Object-relational mapping (ORM) – если кратко, то это такая технология программирования, которая связывает между собой базу данных с концепцией объектно-ориентированного программирования.
По сути, речь идет о прослойке кода между программистом, которому необходимо создавать, получать, изменять и удалять данные (так называемый набор CRUD операций), и таблицами в базе данных, но при этом без написания непосредственно SQL запросов. Вся работа в ORM строится через объекты классов.
В Python есть несколько популярных ORM. В частности, есть Django ORM, которая является составляющей большого фреймворка Django, есть SQLAlchemy и Peewee.
Не смотря на то что Django ORM мне знаком, тащить его в проект Telegram бота — это такое себе решение: уж слишком большая, во всех отношениях, зависимость. Выбирая между Peewee и SQLAlchemy, выбор пал на Peewee. Даже не спрашивайте почему — выбор был интуитивный.
После установки Peewee в виртуальное окружение можно приступить к формированию первой модели, на основе которой будет создана таблица, где будут храниться названия регионов и еще немного дополнительной информации.
В пакете bot создаю директорию database, где и будут находиться модули, отвечающие за работу с базой данных. Первым таким модулем будет модуль models.py, в котором я размещу код моделей.
Модель представляет собой класс, который наследуется от класса Model. Согласно документации, название класса моделей необходимо писать в единственном числе
Следовательно, в моем случае класс моделей будет называться Region.
По умолчанию, созданная таблица будет названа так же, как и класс модели, на основе которого она сформирована. Это поведение можно переопределить в meta классе модели. Я пока оставлю базовое поведение, потом посмотрим.
Вот, что у меня получилось в итоге:
from peewee import ( CharField, IntegerField, Model, SmallIntegerField, SqliteDatabase, ) db_work_for_everyone = SqliteDatabase('db_for_dev.db') class Region(Model): """Модель для хранения данных регионов.""" id = IntegerField(primary_key=True) region_name = CharField(unique=True, help_text='Название региона') region_code = SmallIntegerField(unique=True, help_text='Код региона') federal_district_code = SmallIntegerField( help_text='Номер федерального округа' ) class Meta: database = db_work_for_everyone def __str__(self) -> str: return self.region_name
Добавил еще метод __str__() для того, чтобы информативнее были сообщения при отладке и тестировании, да и в целом это очень хорошая практика.
Далее вся работа по совершению CRUD операций с созданной таблице будет осуществляться через объекты класса Region.
Создание таблицы и наполнение ее данными
В отличии от знакомой мне по учебе Django ORM в Peewee отсутствуют миграции. Поэтому создание файла базы данных, применительно к SQLite, и таблиц осуществляется немного иначе.
При первом соединении будет создан файл базы данных. Я назвал его db_for_dev.db. За создание таблицы отвечает метод класса модели create_table().
Таблица для хранения данных о регионах создана, осталось ее наполнить данными. Можно, конечно, вставлять данные по одной записи, но это так себе путь. Поэтому напишу небольшую функцию, которая будет подгружать данные из созданного csv файла в таблицу region. Такой подход очень удобен, особенно в процессе разработки, когда все время что-нибудь меняется и все необходимо начинать заново.
Работа с данными. Модуль views.py
В пакете database создаю еще один модуль, в котором будут находиться все функции, отвечающие за работу с базой данных. По аналогии с Django ORM назову такой модуль views.py.
Прежде чем писать функцию загрузки данных, мне необходимо написать две функции, которые будут отвечать за открытие и закрытие соединения с БД. Согласно документации, явно открывать соединение не требуется, однако это считается хорошей практикой.
Оба метода будут приватные, так как более нигде они не будут использоваться. Я понимаю, что это не исключает возможности импортировать их в другие модули, но тем не менее.
В каждом методе перед открытием или закрытием соединения с БД буду проверять, не открыто ли или не закрыто ли соединение соответственно. Для этого использую два метода объекта SqliteDatabase:
- is_connection_usable() – проверяет, открыто ли соединение с БД;
- is_closed() метод, позволяющий проверить, закрыто соединение с БД или нет.
Соединение с БД наладил, пора загружать в таблицу region данные. В Модуле views.py написал метод load_information_about_regions().
Метод работает довольно просто. С помощью конструкции with open открываю файл information_about_regions.csv, далее, используя встроенный модуль csv, читаю файл и передаю его в объект reader.
Затем циклом for перебираю все записи и сохраняю их в таблице region, используя метод create().
for row in reader: region_code = row[0] region_name = row[1] federal_district_code = row[2] Region.create( region_name=region_name, region_code=region_code, federal_district_code=federal_district_code, )
Но так было до того момента, пока я не начал описывать эту часть кода. Я стараюсь чаще заглядывать в документацию по тем модулям или библиотекам, которые я использую. Конечно, иногда я ленюсь, то тем не менее.
Так вот, в документации к Peewee я нашел метод массовых вставок, который позволяет одним движением вставить сразу большой объем данных. И я подумал, а почему бы и не попробовать!
Речь идет о методе insert_many(). Обязательным параметром метода является параметр rows, который должен быть итерируемым. Например, методу можно передать список словарей, собственно что я и сделал.
Только прежде чем передавать такой итерируемый объект, его надо сгенерировать. Генерировал я такой объект с помощью спискового включения (list comprehensions).
Генерация списка словарей:
rows = [ { key: value for key, value in zip( ['region_code', 'region_name', 'federal_district_code'], one_record ) } for one_record in reader ]
Собственно и все. Я замерил скорость выполнения кода с помощью модуля time, скорость выросла примерно в 30 раз! Оно и понятно, количество инсертов сократилось.
Привожу метод загрузки данных из csv файла целиком:
def load_information_about_regions() -> None: """Загрузка данных о регионах в БД.""" with open( os.path.join(BASE_DIR, 'database', 'information_about_regions.csv') ) as information_about_regions_csv: reader = csv.reader( information_about_regions_csv, delimiter=';', skipinitialspace=True, ) rows = [ { key: value for key, value in zip( ['region_code', 'region_name', 'federal_district_code'], one_record ) } for one_record in reader ] Region.insert_many(rows).execute() logging.info('data has been successfully loaded into the region table')
Возник вопрос!
Интересный вопрос, а в каком месте вызывать метод для загрузки данных в таблицу region? Я решил ответить на этот вопрос следующим образом, правда, для этого понадобился еще один метод.
В основном исполняемом файле бота bot.py в функции main() я добавил вызов метода check_table_region_and_data_exists(). Данный метод:
- открывает соединение с БД;
- проверяет наличие таблицы с именем region;
- если такой таблице нет, создает ее;
- если таблица существует, проверяет, есть ли в ней данные;
- если данных нет, вызывает метод, отвечающий за загрузку данных в таблицу region.
Возможно, кто-то скажет, что этот метод лишний, но я художник, я так вижу! Уверен, что есть другой способ реализовать описанную выше логику, но я пока смог сделать так. Никто не мешает в дальнейшем вернуться и, с учетом нового опыта, что-то переделать.
Весь код проекта, за исключением того, что не должны видеть все, находится в репозитории на GitHub.