Skip to content

Latest commit

 

History

History
502 lines (363 loc) · 22.4 KB

File metadata and controls

502 lines (363 loc) · 22.4 KB

Урок 12. Скрипты-сидеры

Зачем нужны сидеры

В первом модуле мы запускали готовый seed.sql — файл с INSERT-командами который заполнял базу данными для работы. Это и есть сидер (от англ. seed — посеять): скрипт который наполняет базу данных тестовыми или начальными данными.

В реальной разработке сидеры нужны постоянно:

  • Новый разработчик в команде клонирует репозиторий, запускает сидер — и у него готовая рабочая база данных за несколько секунд
  • Тестирование требует предсказуемого набора данных: одни и те же пользователи, товары, заказы при каждом прогоне
  • Демонстрация — показать заказчику как работает приложение на реалистичных данных, не на пустой базе
  • Разработка — удобно сбросить базу в чистое состояние и заполнить заново когда что-то сломалось

SQL-сидер (seed.sql) с фиксированными INSERT — простой вариант. Но когда нужны сотни или тысячи строк реалистичных данных, писать их вручную невозможно. Для этого пишут сидер на Python.


Библиотека Faker

Faker — библиотека для генерации реалистичных случайных данных: имён, адресов, email, дат, номеров телефонов и многого другого.

Установка:

pip install faker

Основы работы с Faker

from faker import Faker

fake = Faker('ru_RU')   # локаль — русский язык

print(fake.name())          # Алексей Смирнов
print(fake.email())         # alexey.smirnov@example.com
print(fake.city())          # Новосибирск
print(fake.date_this_year()) # 2024-03-15
print(fake.pyint(min_value=1, max_value=100))  # 42
print(fake.pyfloat(min_value=100, max_value=50000, right_digits=2))  # 15420.75

Faker('ru_RU') создаёт генератор с русской локалью — имена, города и адреса будут на русском языке. Для английских данных используют Faker('en_US') или просто Faker().

Воспроизводимость — seed()

По умолчанию Faker генерирует случайные данные при каждом запуске. Если нужен воспроизводимый результат (одни и те же данные при каждом запуске) — устанавливают seed:

from faker import Faker

fake = Faker('ru_RU')
Faker.seed(42)   # фиксируем генератор

print(fake.name())   # всегда одно и то же имя при seed=42
print(fake.name())   # следующее имя — тоже всегда одинаковое

Это полезно для тестов где важна предсказуемость данных.


Стратегии заполнения базы

Прежде чем писать сидер нужно решить как он будет работать с существующими данными. Есть две основные стратегии.

Стратегия 1: Очистить и заполнить заново

Перед вставкой данных таблицы очищаются. База всегда приходит в одно и то же чистое состояние:

def clear_tables(cursor):
    # Порядок важен: сначала зависимые таблицы
    cursor.execute('DELETE FROM order_items')
    cursor.execute('DELETE FROM orders')
    cursor.execute('DELETE FROM products')
    cursor.execute('DELETE FROM users')
    cursor.execute('DELETE FROM categories')

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

Минусы: уничтожает все существующие данные включая те что могли быть важны.

Когда использовать: разработка и тестирование где нужна "чистая" база.

Стратегия 2: Дополнить существующие данные

Данные добавляются к уже существующим без очистки:

def seed_additional_users(cursor, count=10):
    fake = Faker('ru_RU')
    users = [
        (fake.name(), fake.unique.email(), fake.city(), str(fake.date_this_year()))
        for _ in range(count)
    ]
    cursor.executemany(
        'INSERT INTO users (name, email, city, created_at) VALUES (?, ?, ?, ?)',
        users
    )

fake.unique.email() — гарантирует что каждый сгенерированный email уникален в рамках одного запуска. Это важно для столбцов с UNIQUE-ограничением.

Плюсы: существующие данные сохраняются, можно добавлять данные постепенно.

Минусы: при повторном запуске данные накапливаются, возможны конфликты уникальности.

Когда использовать: когда нужно добавить данные к уже существующим, например нагрузочное тестирование.


Пишем полный сидер

Напишем сидер для нашей базы shop.db который генерирует реалистичные данные для всех таблиц. Сидер использует стратегию "очистить и заполнить заново".

Структура скрипта

seed.py
├── import-ы
├── Константы (количество записей)
├── Функции генерации данных для каждой таблицы
├── Функция очистки таблиц
├── Основная функция запуска
└── Точка входа

Полный код сидера

import sqlite3
from faker import Faker

# -----------------------------------------------------------------
# Константы — количество записей
# -----------------------------------------------------------------
NUM_USERS     = 20
NUM_PRODUCTS  = 30
NUM_ORDERS    = 25

# -----------------------------------------------------------------
# Инициализация Faker
# -----------------------------------------------------------------
fake = Faker('ru_RU')
Faker.seed(42)   # воспроизводимый результат


# -----------------------------------------------------------------
# Очистка таблиц
# -----------------------------------------------------------------
def clear_tables(cursor):
    """Удаляет все строки из таблиц в правильном порядке."""
    cursor.execute('DELETE FROM order_items')
    cursor.execute('DELETE FROM orders')
    cursor.execute('DELETE FROM products')
    cursor.execute('DELETE FROM users')
    cursor.execute('DELETE FROM categories')
    print('Таблицы очищены.')


# -----------------------------------------------------------------
# Генерация категорий
# -----------------------------------------------------------------
def seed_categories(cursor):
    """Вставляет фиксированный список категорий."""
    categories = [
        ('Электроника',),
        ('Периферия',),
        ('Мебель',),
        ('Книги',),
        ('Одежда',),
    ]
    cursor.executemany('INSERT INTO categories (name) VALUES (?)', categories)
    print(f'Категорий добавлено: {len(categories)}')


# -----------------------------------------------------------------
# Генерация пользователей
# -----------------------------------------------------------------
def seed_users(cursor, count=NUM_USERS):
    """Генерирует пользователей с уникальными email."""
    users = [
        (
            fake.name(),
            fake.unique.email(),
            fake.city(),
            str(fake.date_between(start_date='-2y', end_date='today'))
        )
        for _ in range(count)
    ]
    cursor.executemany(
        'INSERT INTO users (name, email, city, created_at) VALUES (?, ?, ?, ?)',
        users
    )
    print(f'Пользователей добавлено: {count}')


# -----------------------------------------------------------------
# Генерация товаров
# -----------------------------------------------------------------
def seed_products(cursor, count=NUM_PRODUCTS):
    """Генерирует товары распределённые по категориям."""

    # Получаем id категорий из базы — не хардкодим числа
    cursor.execute('SELECT id FROM categories')
    category_ids = [row[0] for row in cursor.fetchall()]

    product_names = [
        'Ноутбук', 'Смартфон', 'Планшет', 'Монитор', 'Клавиатура',
        'Мышь', 'Наушники', 'Веб-камера', 'Роутер', 'Принтер',
        'Стол', 'Кресло', 'Полка', 'Тумба', 'Лампа',
        'Футболка', 'Худи', 'Джинсы', 'Куртка', 'Шапка',
        'Книга по Python', 'Книга по SQL', 'Книга по алгоритмам',
        'Коврик для мыши', 'USB-хаб', 'Подставка', 'Кабель HDMI',
        'Чехол для ноутбука', 'Сумка', 'Рюкзак',
    ]

    products = []
    for i in range(count):
        name = product_names[i % len(product_names)]
        # Добавляем номер чтобы имена не дублировались
        full_name = f'{name} {fake.bothify("##??").upper()}'
        products.append((
            full_name,
            round(fake.pyfloat(min_value=500, max_value=80000, right_digits=2), 2),
            fake.pyint(min_value=0, max_value=100),
            fake.random_element(category_ids)
        ))

    cursor.executemany(
        'INSERT INTO products (name, price, stock, category_id) VALUES (?, ?, ?, ?)',
        products
    )
    print(f'Товаров добавлено: {count}')


# -----------------------------------------------------------------
# Генерация заказов и позиций
# -----------------------------------------------------------------
def seed_orders(cursor, count=NUM_ORDERS):
    """Генерирует заказы и позиции заказов."""

    statuses = ['pending', 'shipped', 'delivered', 'cancelled']

    # Получаем id пользователей и товаров из базы
    cursor.execute('SELECT id FROM users')
    user_ids = [row[0] for row in cursor.fetchall()]

    cursor.execute('SELECT id, price FROM products')
    products = cursor.fetchall()   # список кортежей (id, price)

    orders = []
    for _ in range(count):
        orders.append((
            fake.random_element(user_ids),
            fake.random_element(statuses),
            str(fake.date_between(start_date='-1y', end_date='today'))
        ))

    cursor.executemany(
        'INSERT INTO orders (user_id, status, created_at) VALUES (?, ?, ?)',
        orders
    )

    # Получаем id только что вставленных заказов
    cursor.execute('SELECT id FROM orders')
    order_ids = [row[0] for row in cursor.fetchall()]

    # Генерируем позиции: 1–3 товара на заказ
    order_items = []
    for order_id in order_ids:
        # Случайные товары без повторений в одном заказе
        num_items = fake.pyint(min_value=1, max_value=3)
        selected = fake.random_elements(products, length=num_items, unique=True)
        for product_id, price in selected:
            order_items.append((
                order_id,
                product_id,
                fake.pyint(min_value=1, max_value=4),
                round(price, 2)
            ))

    cursor.executemany(
        'INSERT INTO order_items (order_id, product_id, quantity, price_at_time) VALUES (?, ?, ?, ?)',
        order_items
    )
    print(f'Заказов добавлено: {count}, позиций: {len(order_items)}')


# -----------------------------------------------------------------
# Проверка результата
# -----------------------------------------------------------------
def print_stats(cursor):
    """Выводит количество строк в каждой таблице."""
    tables = ['categories', 'users', 'products', 'orders', 'order_items']
    print('\n--- Статистика базы данных ---')
    for table in tables:
        cursor.execute(f'SELECT COUNT(*) FROM {table}')
        count = cursor.fetchone()[0]
        print(f'{table:15}: {count} строк')


# -----------------------------------------------------------------
# Точка входа
# -----------------------------------------------------------------
def main():
    with sqlite3.connect('shop.db') as connection:
        cursor = connection.cursor()

        clear_tables(cursor)
        seed_categories(cursor)
        seed_users(cursor)
        seed_products(cursor)
        seed_orders(cursor)
        print_stats(cursor)

        print('\nБаза данных успешно заполнена.')


if __name__ == '__main__':
    main()

Разбор ключевых решений

Получение id из базы вместо хардкода:

cursor.execute('SELECT id FROM categories')
category_ids = [row[0] for row in cursor.fetchall()]

Сидер не знает заранее какие id получат категории — AUTOINCREMENT решает это сам. Поэтому сначала вставляем категории, потом читаем их id из базы и используем при генерации товаров. Это важная практика — сидер не должен угадывать id.

fake.unique.email() для уникальных значений:

fake.unique.email()

Атрибут .unique гарантирует что каждое последующее значение будет отличаться от всех предыдущих в рамках одного запуска. Используйте его для столбцов с UNIQUE-ограничением.

fake.random_elements(..., unique=True) для позиций заказа:

selected = fake.random_elements(products, length=num_items, unique=True)

unique=True гарантирует что в одном заказе один товар не встретится дважды — это соответствует реальной логике и ограничению UNIQUE (order_id, product_id) если оно объявлено.

fake.bothify("##??") для названия товара:

name = product_names[i % len(product_names)]
full_name = f'{name} {fake.bothify("##??").upper()}'

Для товаров используется заранее подготовленный список реалистичных названий (Ноутбук, Смартфон, Кресло, Книга по Python и т.д.), а затем к каждому названию добавляется случайный суффикс через fake.bothify("##??").

Такой подход выбран специально. Хотя Faker умеет генерировать случайные слова (fake.word()) и предложения (fake.sentence()), результат обычно плохо подходит для интернет-магазина: получаются абстрактные или бессмысленные названия, не похожие на реальные товары.

Метод bothify() заменяет специальные символы в строке случайными значениями:

Символ Заменяется на
# случайную цифру от 0 до 9
? случайную букву от a до z

bothify() объединяет возможности двух более простых методов:

  • numerify(): Заменяет только #
  • lexify(): Заменяет только ?

fake.date_between(start_date='-2y', end_date='today') установление диапазона дат:

str(fake.date_between(start_date='-2y', end_date='today'))

Здесь '-2y' и 'today' - специальные значения, которые Faker умеет интерпретировать. Оба параметра метода date_between могут принимать объект date, объект datetime, специальные строки.

В сидерах и тестовых данных чаще всего встречаются:

'-30d'
'-6m'
'-1y'
'-2y'
'today'
'now'

Запуск и проверка

Запуск сидера

python seed.py

Вывод:

Таблицы очищены.
Категорий добавлено: 5
Пользователей добавлено: 20
Товаров добавлено: 30
Заказов добавлено: 25, позиций: 52

--- Статистика базы данных ---
categories     : 5 строк
users          : 20 строк
products       : 30 строк
orders         : 25 строк
order_items    : 52 строк

База данных успешно заполнена.

Проверка через Python

После запуска можно быстро проверить данные:

import sqlite3

with sqlite3.connect('shop.db') as connection:
    connection.row_factory = sqlite3.Row
    cursor = connection.cursor()

    # Проверяем несколько пользователей
    cursor.execute('SELECT name, email, city FROM users LIMIT 5')
    for row in cursor:
        print(f'{row["name"]} | {row["email"]} | {row["city"]}')

Вопросы

  1. Что такое сидер и в каких ситуациях он необходим?
  2. Чем Python-сидер лучше статичного seed.sql с фиксированными INSERT?
  3. Что делает Faker.seed(42) и зачем это нужно?
  4. В чём разница между fake.email() и fake.unique.email()?
  5. Почему в сидере для получения category_ids используется SELECT после вставки категорий, а не фиксированный список [1, 2, 3, 4, 5]?
  6. Какой порядок очистки таблиц правильный и почему?
  7. Когда использовать стратегию "очистить и заполнить" а когда "дополнить"?
  8. Как Faker помогает избежать конфликтов уникальности при генерации данных?
  9. Почему в функции seed_products имена товаров генерируются не через fake.word() или fake.sentence()?
  10. Что произойдёт если запустить сидер со стратегией "дополнить" дважды подряд?

Задачи

Задача 1.

Установите Faker и напишите скрипт который генерирует и печатает 5 имён, 5 email и 5 городов на русском языке.


Задача 2.

Напишите функцию generate_users(count) которая возвращает список кортежей для вставки в таблицу users. Используйте fake.unique.email(). Выведите первые три элемента результата.


Задача 3.

Напишите функцию seed_users(cursor, count) которая вставляет сгенерированных пользователей через executemany(). Проверьте результат через SELECT COUNT(*).


Задача 4.

Напишите функцию clear_and_seed(connection) которая очищает таблицы users и products в правильном порядке (сначала зависимые), затем вставляет по 10 новых записей в каждую.


Задача 5.

Напишите сидер с функцией print_stats(cursor) которая выводит количество строк в каждой таблице базы shop.db. Запустите её до и после заполнения данными.


Задача 6.

Используя стратегию "дополнить", напишите функцию add_products(cursor, count) которая добавляет новые товары к существующим. Запустите её дважды и убедитесь что количество товаров каждый раз увеличивается.


Задача 7.

Напишите полный сидер для базы shop.db с функциями для каждой таблицы, очисткой и выводом статистики. Используйте Faker.seed(10) для воспроизводимости. Количество записей: 10 пользователей, 15 товаров, 12 заказов.


Предыдущий урок | Следующий урок