🎯 Общая идея проекта
Практикум 21 — это пошаговое создание консольного приложения «Онлайн-магазин книг». Программа работает с MySQL и в будущих частях будет дополнена MongoDB. В первой части мы реализуем фундамент:
- Создание базы данных
bookstore. - Создание таблиц
booksиusers. - Главное меню: загрузка книг, регистрация, вход, выход.
- Подменю пользователя после успешного входа.
🗄️ Этап 1. Создание базы данных
Перед созданием таблиц нужна база данных. В MySQL база данных — это именованное пространство, в котором живут таблицы. Чтобы не падать с ошибкой, если база уже существует, используем IF NOT EXISTS:
import pymysql
config = {
"host": "ich-edit.edu.itcareerhub.de",
"user": "ich1",
"password": "ich1_password_ilovedbs",
}
db_name = "bookstore"
with pymysql.connect(**config) as connection:
with connection.cursor() as cursor:
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {db_name}")
cursor.execute("SHOW DATABASES")
databases = [row[0] for row in cursor]
if db_name in databases:
print(f"Database '{db_name}' created or already exists.")
Обратите внимание: имя базы данных здесь вставлено f-строкой. Это допустимо, потому что db_name — константа, заданная программистом, а не пользовательский ввод. Для любых пользовательских данных всегда используйте параметризованные запросы.
📋 Этап 2. Создание таблиц
Таблица books хранит книги, таблица users — зарегистрированных клиентов. Поле id делаем автоинкрементным первичным ключом:
CREATE TABLE IF NOT EXISTS books (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200),
author VARCHAR(100),
price DECIMAL(10, 2),
stock INT CHECK (stock >= 0)
);
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100),
password VARCHAR(100),
balance DECIMAL(10, 2) CHECK (balance >= 0)
);
Тип DECIMAL(10, 2) выбран для денежных значений: он хранит числа с фиксированной точностью, в отличие от FLOAT, где возможны погрешности. CHECK-ограничения защищают от отрицательного количества книг и отрицательного баланса.
После создания таблиц можно вывести их список:
cursor.execute(f"USE {db_name}")
cursor.execute("SHOW TABLES")
tables = [row[0] for row in cursor]
print(f"Tables in '{db_name}':")
for table in tables:
print(f"- {table}")
🖥️ Этап 3. Главное меню
Меню — бесконечный цикл while True, который предлагает пользователю выбрать действие. Выход из цикла происходит при выборе пункта 0. Для каждого действия вызывается отдельная функция, которой передаётся объект соединения connection.
with pymysql.connect(**config) as connection:
while True:
print("\n--- Bookstore Menu ---")
print("1. Load books from file")
print("2. Register new user")
print("3. Login as user")
print("0. Exit")
choice = input("Choose action: ")
if choice == "1":
filename = input("Enter file name: ")
load_books_from_file(filename, connection)
elif choice == "2":
register_user(connection)
elif choice == "3":
user_id = try_login(connection)
if user_id:
user_menu(connection, user_id)
elif choice == "0":
break
else:
print("Invalid option.")
📁 Этап 3.1. Загрузка книг из CSV-файла
Файл с книгами имеет простой формат: каждая строка — четыре поля через запятую. Пример:
1984,George Orwell,8.99,5
Brave New World,Aldous Huxley,9.50,3
Алгоритм загрузки:
- Открыть файл с кодировкой UTF-8.
- Для каждой строки разбить по запятой и проверить, что получилось ровно 4 части.
- Преобразовать цену в
float, количество вint. - Проверить, есть ли книга с таким названием и автором.
- Если есть — обновить количество, иначе — вставить новую запись.
- После обработки всех строк вызвать
connection.commit().
def load_books_from_file(filename: str, connection) -> None:
added = 0
with open(filename, encoding="utf-8") as file:
with connection.cursor() as cursor:
cursor.execute(f"USE {db_name}")
for line in file:
parts = line.strip().split(",")
if len(parts) != 4:
continue
title, author, price, stock = parts
price = float(price)
stock = int(stock)
cursor.execute(
"SELECT id, stock FROM books WHERE title = %s AND author = %s",
(title, author),
)
result = cursor.fetchone()
if result:
book_id, current_stock = result
cursor.execute(
"UPDATE books SET stock = %s WHERE id = %s",
(current_stock + stock, book_id),
)
else:
cursor.execute(
"INSERT INTO books (title, author, price, stock) VALUES (%s, %s, %s, %s)",
(title, author, price, stock),
)
added += 1
connection.commit()
print(f"{added} books loaded.")
csv из стандартной библиотеки вместо ручного split(",").
📝 Этап 3.2. Регистрация клиента
Пользователь вводит логин, пароль и начальный баланс. Перед вставкой проверяем, что логин ещё не занят. Это типичный guard clause: если условие не выполнено — выходим из функции раньше, без вложенных блоков.
def register_user(connection) -> None:
username = input("Enter username: ")
password = input("Enter password: ")
try:
balance = float(input("Enter initial balance: "))
except ValueError:
print("Invalid balance.")
return
with connection.cursor() as cursor:
cursor.execute(f"USE {db_name}")
cursor.execute("SELECT id FROM users WHERE username = %s", (username,))
if cursor.fetchone():
print("Username already exists.")
return
cursor.execute(
"INSERT INTO users (username, password, balance) VALUES (%s, %s, %s)",
(username, password, balance),
)
connection.commit()
print("Registration successful.")
bcrypt или hashlib с солью.
🔐 Этап 3.3. Вход в аккаунт
Вход сводится к поиску пользователя с заданным логином и паролем. Если запись найдена — возвращаем user_id, иначе None. После успешного входа запускается подменю клиента.
def try_login(connection) -> int | None:
username = input("Enter username: ")
password = input("Enter password: ")
with connection.cursor() as cursor:
cursor.execute(f"USE {db_name}")
cursor.execute(
"SELECT id FROM users WHERE username = %s AND password = %s",
(username, password),
)
result = cursor.fetchone()
if result:
print("Login successful.")
return result[0]
print("Invalid username or password.")
return None
def user_menu(connection, user_id: int) -> None:
while True:
print(f"\n--- User Menu (ID {user_id}) ---")
print("1. View available books")
print("0. Logout")
choice = input("Choose action: ")
if choice == "1":
print("Books will be shown here.")
elif choice == "0":
break
else:
print("Invalid option.")
Обратите внимание на ранний возврат: если result равен None, функция сразу сообщает об ошибке и возвращает None, не уходя в длинный if/else.
✨ Современные практики
- f-strings: удобны для вывода сообщений, но не для формирования SQL-запросов с пользовательскими данными.
- Guard clauses: проверяйте ошибочные условия и сразу выходите из функции — код остаётся плоским и читаемым.
- Ранний возврат: возвращайте результат сразу, как только он известен, вместо вложенных блоков.
- Параметризованные запросы: передавайте значения вторым аргументом в
execute(). Библиотека сама экранирует опасные символы. - Type hints: аннотации
(filename: str) -> Noneпомогают IDE и читателю кода понимать ожидаемые типы. if __name__ == "__main__": запускайте меню только при прямом запуске файла, чтобы функции можно было импортировать в тесты или другие модули.
.env, а код разбивают на модули: config.py, db.py, models.py, main.py.