Ни для кого не новость, что большинство сегодняшних приложений взаимодействуют с базами данных. Особенно с движками на основе RDBMS (движки DB с поддержкой SQL). Как и любой другой язык программирования, Pyhton также предоставляет как собственные библиотеки для взаимодействия с базами данных, так и от третьих лиц. Как правило, вам нужно прописать запросы SQL для CRUD операций. Это нормально, однако иногда получается мешанина:
- Шеф решил перейти с MySQL в… MSSQL и у вас нет выбора, кроме как кивнуть и внести правки в свои запросы в соответствии с другим движком баз данных;
- Вам нужно сделать несколько запросов, чтобы получить одну часть данных из другой таблицы;
- Список можно продолжать долго, не так ли?
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Чтобы разобраться с этим, в игру вступает ORM.
Введение в ORM
ORM – это акроним от Object Relational Mapping (Объектно-реляционное отображение). Но что именно оно делает?
Из википедии:
Объектно-реляционное отображение — это технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных». Существуют как проприетарные, так и свободные реализации этой технологии.
Звучит круто, да?
Что такое Peewee?
Peewee (http://docs.peewee-orm.com/en/latest/) – это небольшое ORM, которое в данный момент поддерживает postgresql, mysql и sqlite. Разумеется, это не единственное ORM для разработчиков Python. К примеру, Django предоставляет собственную ORM библиотеку, кроме этого, всегда есть SqlAlchemy. Хорошая сторона Peewee – это то, что он занимает мало места, его легко освоить, и вы можете приступить к работе с приложениями за несколько минут.
Достаточно слов, перейдем к кодам!
Установка Peewee
Как и многие другие библиотеки Python, вы можете установить Peewee при помощи pip:
1 |
pip install peewee |
Настройка базы данных
Как я говорил, Peewee поддерживает ряд движков для работы с базами данных (полный список тут: http://docs.peewee-orm.com/en/latest/peewee/database.html), в данной статье я использую MySQL.
1 2 3 4 5 6 7 8 9 10 11 |
from peewee import * user = 'root' password = 'root' db_name = 'peewee_demo' dbhandle = MySQLDatabase( db_name, user=user, password=password, host='localhost' ) |
Объект MySQLDatabase создан.
Создание моделей в Peewee
Сейчас я перейду к созданию моделей. В этой статье я использую две таблицы или модели: «Категория» и «Продукт«. Категория может содержать несколько продуктов. Сохраняем как файл models.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from peewee import * class BaseModel(Model): class Meta: database = dbhandle class Category(BaseModel): id = PrimaryKeyField(null=False) name = CharField(max_length=100) created_at = DateTimeField(default=datetime.datetime.now()) updated_at = DateTimeField(default=datetime.datetime.now()) class Meta: db_table = "categories" order_by = ('created_at',) |
Сначала я создал BaseModel. Это связанно с тем, что Peewee просит вас передать dbhandle в каждый класс Model. Чтобы избежать лишних движений, я просто создал базовый класс и расширил его. Ознакомиться со всеми типами столбцов в таблице можно тут: http://docs.peewee-orm.com/en/latest/peewee/models.html#field-types-table
Хорошо, наша BaseModel и Category созданы. Модель Category состоит из четырех полей:
- id, который является полем автоматического прироста;
- name содержит имя категории;
- updated_at и created_at – поля timestamp, которые определяют настоящее время по умолчанию.
В классе Meta я передаю название таблицы в собственность db_table. Это не обязательно, если название таблицы и модели одинаковые. Собственность order_by указывает, какой столбец должен использоваться для сортировки данных во время извлечения. Вы можете переписать его, передав вид по полю на ваше усмотрение.
Перед тем как мы двинемся дальше, я хочу создать еще один файл под названием operations.py, в котором я буду использовать эти модели.
1 2 3 4 5 6 7 8 9 |
import peewee from models import * if __name__ == '__main__': try: dbhandle.connect() Category.create_table() except peewee.InternalError as px: print(str(px)) |
После импорта, я подключаюсь к базе данных. Ошибка peewee.OperationalError ссылается на все ошибки, связанные с Peewee. К примеру, если вы введете неправильные учетные данные, вы получите следующее:
1 |
(1045, "Access denied for user 'root1'@'localhost' (using password: YES)") |
Затем мы вызываем Category.create_table(), которые создает таблицу с указанной ранее собственностью. Если вы передаете safe=True в качестве параметра, то существующая таблица просто перепишется. Это может привести к проблемам в реальной ситуации.
Далее, модель Product:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Product(BaseModel): id = PrimaryKeyField(null=False) name = CharField(max_length=100) price = FloatField(default=None) category = ForeignKeyField(Category, related_name='fk_cat_prod', to_field='id', on_delete='cascade', on_update='cascade') created_at = DateTimeField(default=datetime.datetime.now()) updated_at = DateTimeField(default=datetime.datetime.now()) class Meta: db_table = "products" order_by = ('created_at',) |
Она аналогична модели Cateogory. Разница только в ForeignKeyField, который указывает, как именно Product должен быть связан с Category. Обновление основы должно выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 |
if __name__ == '__main__': try: dbhandle.connect() Category.create_table() except peewee.InternalError as px: print(str(px)) try: Product.create_table() except peewee.InternalError as px: print(str(px)) |
После запуска указанного выше кода, создается таблица модели product, а также отношение с таблицей categories. Вот скриншот моего клиента sql:
Вставка записи (INSERT)
Теперь мы можем перейти к добавлению данных сперва в category, а затем в таблицу products. Так как у нас есть рабочие модели, не так просто добавлять или обновлять записи.
1 2 3 4 5 6 7 8 9 10 |
import peewee from models import * def add_category(name): row = Category( name=name.lower().strip(), ) row.save() add_category('Books') |
Я добавил функцию под названием add_category() с параметром и именем внутри. Объект Category создан, как и поля таблицы, которые являются переданной собственностью данного объекта класса. В нашем случае, это поле name.
The row.save() сохраняет данные из объекта в базу данных.
Круто, не так ли? Больше не нужно прописывать уродливые INSERT-ы.
Теперь добавим product.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import peewee from models import * def add_product(name, price, category_name): cat_exist = True try: category = Category.select().where(Category.name == category_name.strip()).get() except DoesNotExist as de: cat_exist = False if cat_exist: row = Product( name=name.lower().strip(), price=price, category=category ) row.save() |
add_product берет name, price и category_id в качестве вводных данных. Сначала, я проверю, существует ли категория, если да – значит её объекты хранятся в базе. В ORM вы имеете дело с объектом, по этому вы передаете информацию о категории в качестве объекта, так как мы уже определили эту взаимосвязь ранее.
Далее, я буду создавать разделы в main.py:
1 2 |
add_category('Books') add_category('Electronic Appliances') |
Теперь добавим продукты:
1 2 3 |
# Добавление продуктов. add_product('C++ Premier', 24.5, 'books') add_product('Juicer', 224.25, 'Electronic Appliances') |
Полный main.py можете видеть ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import peewee from models import * def add_category(name): row = Category( name=name.lower().strip(), ) row.save() def add_product(name, price, category_name): cat_exist = True try: category = Category.select().where(Category.name == category_name.strip()).get() except DoesNotExist as de: cat_exist = False if cat_exist: row = Product( name=name.lower().strip(), price=price, category=category ) row.save() if __name__ == '__main__': # Создаем разделы. add_category('Books') add_category('Electronic Appliances') # Добавляем продукты в разделы. add_product('C++ Premier', 24.5, 'books') add_product('Juicer', 224.25, 'Electronic Appliances') |
Я передаю имя категории, как только её объект будет найден и передан объекту класса Product. Если вы хотите пойти по пути SQL, для начала вам нужно выполнить SELECT, чтобы получить существующий category_id, и затем назначить id добавляемому продукту.
Так как работать с ORM – значит иметь дело с объектами, мы храним объекты вместо скалярных значений. Кто-то может посчитать это слишком «инженерным» методом в нашем случае, но подумайте о случаях, когда вы понятия не имеете о том, какая база данных должна быть использована в будущем. Ваш код – это универсальный инструмент для работы с базами данных, так что если он работает с MySQL, или MSSQL, то он будет работать даже с MongoDb (гипотетически).
Выбор нескольких записей
Сначала выделяем категории:
1 2 3 4 5 6 7 8 9 |
import peewee from models import * def find_all_categories(): return Category.select() def find_all_products(): return Product.select() |
Category.select() возвращает ВСЮ запись, которая будет отсортирована по столбцу created_at, что мы и указали в классе Meta.
Теперь выбираем все продукты:
1 2 3 4 5 6 7 8 9 10 |
products = find_all_products() product_data = [] for product in products: product_data.append({ 'title': product.name, 'price': product.price, 'category': product.category.name }) print(product_data) |
Здесь я перебираю продукты и добавляю запись в список product_data. Обратите внимание на доступ к категории продукта. Больше нет SELECT для получения ID, с последующим поиском названия. Простой цикл делает все за нас. При запуске, информация о продукте будет отображаться следующим образом:
1 2 3 4 |
[ {'price': 24.5, 'title': 'c++ premier', 'category': 'books'}, {'price': 224.25, 'title': 'juicer', 'category': 'electronic appliances'} ] |
Выбор одной записи
Чтобы выбрать одну запись, вам нужно использовать метод get:
1 2 |
def find_product(name): return Product.get(Product.name == name.lower().strip()) |
Теперь это называется так:
1 2 |
p = find_product('c++ premier') print(p.category.name) |
Название товара передано функции find_product, если запись существует – она вернет экземпляр Product. Здесь я вывожу категорию, связанную с этим продуктом.
Обновление записей в Peewee
Обновление записей – это так же просто, как и их создание. Вы получаете экземпляр объекта, после чего обновляете его.
1 2 3 4 5 6 7 |
import peewee from models import * def update_category(id, new_name): category = Category.get(Category.id == id) category.name = new_name category.save() |
После этого, вызываем его как:
1 |
update_category(2, 'Kindle Books') |
Удаление записей в Peewee
Удаление записи не сильно отличается от обновления:
1 2 3 4 5 6 |
import peewee from models import * def delete_category(name): category = Category.get(Category.name == name.lower().strip()) category.delete_instance() |
Вызываем данную функцию для удаления раздела:
1 |
delete_category('Kindle Books') |
Обратите внимание на то, что после удаления записи, вы также удаляете связанные продукты, так как они подключены к категории, и мы уже определили сценарий во время удаления раздела ON_DELETE, будут удаляться и товары из раздела.
Готовое приложение
Сохраняем данные криптовалют от биржи Binance
Допустим у нас уже есть MySQL база данных и существует таблица: coins. Нам нужно создать модель для этой существующей таблице и сохранить полученные json данные от Binance.
Структура базы данных:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
CREATE TABLE `coins` ( `id` int(11) NOT NULL, `symbol` varchar(20) NOT NULL, `priceChange` float NOT NULL, `priceChangePercent` float NOT NULL, `weightedAvgPrice` float NOT NULL, `prevClosePrice` float NOT NULL, `lastPrice` float NOT NULL, `lastQty` float NOT NULL, `bidPrice` float NOT NULL, `askPrice` float NOT NULL, `openPrice` float NOT NULL, `highPrice` float NOT NULL, `lowPrice` float NOT NULL, `volume` float NOT NULL, `quoteVolume` float NOT NULL, `openTime` int(30) NOT NULL, `closeTime` int(30) NOT NULL, `firstId` int(10) NOT NULL, `lastId` int(10) NOT NULL, `count` int(10) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
Данные от Binance мы получим по ссылке: https://api.binance.com/api/v1/ticker/24hr для HTTP запроса мы будет использовать библиотеку Requests на примере.
Что мы реализуем в нашем приложении:
- Добавление новых монет;
- Проверка если монета существует;
- Обновления данных по цене если монета существует;
- Сортируем монеты по объему за 24 часа;
- Кол-во монет которые торгуются с BNB в паре (Binance Coin);
- Выбираем случайные 5 монет
Исходный код
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
import requests from peewee import MySQLDatabase, Model from peewee import IntegerField, FloatField, CharField, PrimaryKeyField, TimestampField, fn from peewee import InternalError db = MySQLDatabase( 'peewee', user='root', password='mangos', host='localhost' ) class Coins(Model): id = PrimaryKeyField(null=False) symbol = CharField(unique=True) priceChange = FloatField() priceChangePercent = FloatField() weightedAvgPrice = FloatField() prevClosePrice = FloatField() lastPrice = FloatField() lastQty = FloatField() bidPrice = FloatField() askPrice = FloatField() openPrice = FloatField() highPrice = FloatField() lowPrice = FloatField() volume = FloatField() quoteVolume = FloatField() openTime = TimestampField() closeTime = TimestampField() firstId = IntegerField() lastId = IntegerField() count = IntegerField() class Meta: db_table = 'coins' database = db # Создаем таблицу если не существует. try: db.connect() Coins.create_table() except InternalError as px: print(str(px)) # Получаем список криптовалют от Binance. data = requests.get('https://api.binance.com/api/v1/ticker/24hr').json() for coin in data: # Проверяем если валюта существует. exists = Coins.select().where(Coins.symbol == coin['symbol']) if bool(exists): print('Обновляем цены для:', coin['symbol']) # Запись существует. # Обновляем цены криптовалют. Coins.update( lastPrice=coin['lastPrice'], lastQty=coin['lastQty'], bidPrice=coin['bidPrice'], askPrice=coin['askPrice'], ).where(Coins.symbol == coin['symbol']).execute() else: print('Добавляем новую запись:', coin['symbol']) # Создаем новую запись. Coins.create( symbol=coin['symbol'], priceChange=coin['priceChange'], priceChangePercent=coin['priceChangePercent'], weightedAvgPrice=coin['weightedAvgPrice'], prevClosePrice=coin['prevClosePrice'], lastPrice=coin['lastPrice'], lastQty=coin['lastQty'], bidPrice=coin['bidPrice'], askPrice=coin['askPrice'], openPrice=coin['openPrice'], highPrice=coin['highPrice'], lowPrice=coin['lowPrice'], volume=coin['volume'], quoteVolume=coin['quoteVolume'], openTime=int(coin['openTime'] / 1000), closeTime=int(coin['closeTime'] / 1000), firstId=coin['firstId'], lastId=coin['lastId'], count=coin['count'], ) # Сортируем монеты по объему за 24 часа. sorted_coins = Coins.select().order_by(Coins.volume.desc()) print('Сортировка по объему за 24 часа') for coin in sorted_coins: print(coin.symbol, coin.volume) print('-' * 30) # Кол-во монет которые торгуются с BNB в паре (Binance Coin). bnb_count = Coins.select().where(Coins.symbol.endswith('BNB')).count() print('Кол-во монет в паре с BNB:', bnb_count) print('-' * 30) # Выбираем случайные 5 монет. random = Coins.select().order_by(fn.Rand()).limit(5) print('Случайные 5 монет:') for coin in random: print(coin.symbol) |
В третьей строке вы можете видеть список типов столбцов из таблицы, такие как Integer, Float и Varchar (IntegerField, FloatField, CharField, PrimaryKeyField, TimestampField). Подробнее про тип полей можете узнать из документации: https://peewee.readthedocs.io/en/2.0.2/peewee/fields.html
Больше примеров которые не были использованы: http://docs.peewee-orm.com/en/latest/peewee/querying.html
Результат который я получил:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/usr/bin/python3.5 /home/database/peewee-test.py Добавляем новую запись: ETHBTC Добавляем новую запись: LTCBTC .... Добавляем новую запись: KEYBTC Добавляем новую запись: KEYETH Сортировка по объему за 24 часа NPXSBTC 1778630000.0 KEYBTC 782103000.0 .... BCCBNB 349.373 REPBNB 70.274 ------------------------------ Кол-во монет в паре с BNB: 70 ------------------------------ Случайные 5 монет: THETABTC POWRBNB IOTABNB AGIBTC STEEMETH Process finished with exit code 0 |
Меняем MySQL на SQLite
Используя пример из кода выше, мы изменим базу данных из MySQL на SQLite не трогая код (кроме самого подключения). Нам нужно обновить немного наш файл. С самых первых строк было:
1 2 3 4 5 6 7 8 9 |
import requests from peewee import MySQLDatabase, Model from peewee import IntegerField, FloatField, CharField, PrimaryKeyField, TimestampField, fn from peewee import InternalError db = MySQLDatabase( 'peewee', user='root', password='mangos', host='localhost' ) |
Теперь у нас:
1 2 3 4 5 6 |
import requests from peewee import SqliteDatabase, Model from peewee import IntegerField, FloatField, CharField, PrimaryKeyField, TimestampField, fn from peewee import InternalError db = SqliteDatabase('binance-coins.db') |
После запуска отредактированного кода мы получим ошибку:
1 |
peewee.OperationalError: no such function: Rand |
Ошибка в примере «Выбираем случайные 5 монет», в коде мы используем fn.Rand() и он работает для MySQL, но для SQLite и PostgreSQL он работать не будет, нужно использовать fn.Random(). Подробнее: http://docs.peewee-orm.com/en/latest/peewee/querying.html#getting-random-records
После запуска у нас появился файл базы данных binance-coins.db
Вывод
Отлично, вы освоили основы Peewee и то, как вы можете использовать эту маленькую и эффективную ORM в ваших следующих проектах, связанных с Pyhton. Если у вас есть дополнительные вопросы – вы можете ознакомиться с официальной документацией и найти в ней ответы.
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»