В первом модуле мы запускали готовый seed.sql — файл с INSERT-командами который заполнял базу данными для работы. Это и есть сидер (от англ. seed — посеять): скрипт который наполняет базу данных тестовыми или начальными данными.
В реальной разработке сидеры нужны постоянно:
- Новый разработчик в команде клонирует репозиторий, запускает сидер — и у него готовая рабочая база данных за несколько секунд
- Тестирование требует предсказуемого набора данных: одни и те же пользователи, товары, заказы при каждом прогоне
- Демонстрация — показать заказчику как работает приложение на реалистичных данных, не на пустой базе
- Разработка — удобно сбросить базу в чистое состояние и заполнить заново когда что-то сломалось
SQL-сидер (seed.sql) с фиксированными INSERT — простой вариант. Но когда нужны сотни или тысячи строк реалистичных данных, писать их вручную невозможно. Для этого пишут сидер на Python.
Faker — библиотека для генерации реалистичных случайных данных: имён, адресов, email, дат, номеров телефонов и многого другого.
Установка:
pip install fakerfrom 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.75Faker('ru_RU') создаёт генератор с русской локалью — имена, города и адреса будут на русском языке. Для английских данных используют Faker('en_US') или просто Faker().
По умолчанию Faker генерирует случайные данные при каждом запуске. Если нужен воспроизводимый результат (одни и те же данные при каждом запуске) — устанавливают seed:
from faker import Faker
fake = Faker('ru_RU')
Faker.seed(42) # фиксируем генератор
print(fake.name()) # всегда одно и то же имя при seed=42
print(fake.name()) # следующее имя — тоже всегда одинаковоеЭто полезно для тестов где важна предсказуемость данных.
Прежде чем писать сидер нужно решить как он будет работать с существующими данными. Есть две основные стратегии.
Перед вставкой данных таблицы очищаются. База всегда приходит в одно и то же чистое состояние:
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')Плюсы: предсказуемость, одинаковый результат при каждом запуске, нет дублирования.
Минусы: уничтожает все существующие данные включая те что могли быть важны.
Когда использовать: разработка и тестирование где нужна "чистая" база.
Данные добавляются к уже существующим без очистки:
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 строк
База данных успешно заполнена.
После запуска можно быстро проверить данные:
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"]}')- Что такое сидер и в каких ситуациях он необходим?
- Чем Python-сидер лучше статичного
seed.sqlс фиксированнымиINSERT? - Что делает
Faker.seed(42)и зачем это нужно? - В чём разница между
fake.email()иfake.unique.email()? - Почему в сидере для получения
category_idsиспользуетсяSELECTпосле вставки категорий, а не фиксированный список[1, 2, 3, 4, 5]? - Какой порядок очистки таблиц правильный и почему?
- Когда использовать стратегию "очистить и заполнить" а когда "дополнить"?
- Как Faker помогает избежать конфликтов уникальности при генерации данных?
- Почему в функции
seed_productsимена товаров генерируются не черезfake.word()илиfake.sentence()? - Что произойдёт если запустить сидер со стратегией "дополнить" дважды подряд?
Установите Faker и напишите скрипт который генерирует и печатает 5 имён, 5 email и 5 городов на русском языке.
Напишите функцию generate_users(count) которая возвращает список кортежей для вставки в таблицу users. Используйте fake.unique.email(). Выведите первые три элемента результата.
Напишите функцию seed_users(cursor, count) которая вставляет сгенерированных пользователей через executemany(). Проверьте результат через SELECT COUNT(*).
Напишите функцию clear_and_seed(connection) которая очищает таблицы users и products в правильном порядке (сначала зависимые), затем вставляет по 10 новых записей в каждую.
Напишите сидер с функцией print_stats(cursor) которая выводит количество строк в каждой таблице базы shop.db. Запустите её до и после заполнения данными.
Используя стратегию "дополнить", напишите функцию add_products(cursor, count) которая добавляет новые товары к существующим. Запустите её дважды и убедитесь что количество товаров каждый раз увеличивается.
Напишите полный сидер для базы shop.db с функциями для каждой таблицы, очисткой и выводом статистики. Используйте Faker.seed(10) для воспроизводимости. Количество записей: 10 пользователей, 15 товаров, 12 заказов.