← Назад к вопросам

Как происходит миграция схемы базы данных в Hibernate

2.2 Middle🔥 81 комментариев
#ORM и Hibernate

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Миграция схемы базы данных в Hibernate

Миграция БД - это процесс изменения структуры базы данных (таблицы, колонки, индексы) при сохранении данных. Hibernate предоставляет инструменты для этого, но важно понимать разные подходы.

Основные подходы к миграции в Hibernate

1. Hibernate Auto Schema Generation (разработка)

# application.properties
spring.jpa.hibernate.ddl-auto=update
# Опции:
# validate - проверяет схему, не меняет
# update - добавляет новые таблицы/колонки
# create - пересоздает все (удаляет данные!)
# create-drop - create на старте, drop на выходе
# none - ничего не делает (нужны вручную скрипты)

❌ ПРОБЛЕМЫ с auto-generation

❌ НЕ БЕЗОПАСНО для production:
- Может удалить важные данные
- Нет rollback если ошибка
- Нет истории изменений
- Не контролируемо и не предсказуемо

❌ Невозможно:
- Переименовать колонку (создаст новую, старую удалит)
- Изменить тип данных сложно
- Добавить custom логику (например индексы)
- Откатить миграцию если понадобилось

2. Liquibase (управляемые миграции)

<!-- pom.xml -->
<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
    <version>4.25.1</version>
</dependency>
# application.yml
spring:
  liquibase:
    change-log: classpath:db/changelog/db.changelog-master.yaml

Changelog файл

# db/changelog/db.changelog-master.yaml
databaseChangeLog:
  - include:
      file: db/changelog/changes/001_initial_schema.yaml
  - include:
      file: db/changelog/changes/002_add_user_email.yaml
  - include:
      file: db/changelog/changes/003_create_orders_table.yaml

Пример миграции

# db/changelog/changes/001_initial_schema.yaml
databaseChangeLog:
  - changeSet:
      id: 1
      author: developer
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: id
                  type: BIGINT
                  autoIncrement: true
                  constraints:
                    primaryKey: true
              - column:
                  name: username
                  type: VARCHAR(255)
                  constraints:
                    nullable: false
              - column:
                  name: email
                  type: VARCHAR(255)
                  constraints:
                    unique: true
              - column:
                  name: created_at
                  type: TIMESTAMP
                  defaultValue: CURRENT_TIMESTAMP

Добавить колонку

# db/changelog/changes/002_add_user_email.yaml
databaseChangeLog:
  - changeSet:
      id: 2
      author: developer
      changes:
        - addColumn:
            tableName: users
            columns:
              - column:
                  name: phone
                  type: VARCHAR(20)
                  constraints:
                    nullable: true

Переименовать колонку

# db/changelog/changes/003_rename_email_field.yaml
databaseChangeLog:
  - changeSet:
      id: 3
      author: developer
      changes:
        - renameColumn:
            tableName: users
            oldColumnName: email_address
            newColumnName: email
            columnDataType: VARCHAR(255)

3. Flyway (альтернатива Liquibase)

<!-- pom.xml -->
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
    <version>9.22.3</version>
</dependency>
# application.yml
spring:
  flyway:
    locations: classpath:db/migration
    baseline-on-migrate: true

Файлы миграций Flyway

db/migration/
  V1__Initial_schema.sql
  V2__Add_users_table.sql
  V3__Add_orders_table.sql
  V4__Add_email_column.sql

SQL миграция

-- db/migration/V1__Initial_schema.sql
CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE posts (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    title VARCHAR(255),
    content TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);
-- db/migration/V2__Add_phone_column.sql
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

Синхронизация с Hibernate моделями

Entity класс

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "username", nullable = false)
    private String username;
    
    @Column(name = "email", unique = true)
    private String email;
    
    @Column(name = "phone")
    private String phone;
    
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Post> posts = new ArrayList<>();
    
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
    }
    
    // getters и setters
}

Best Practice для Production миграций

✅ ШАГ 1: Планирование

1. Определить что меняется в схеме
2. Написать SQL скрипт
3. Протестировать на копии БД
4. Обновить Entity классы в коде
5. Написать тесты для новой схемы

✅ ШАГ 2: Разработка миграции

-- db/migration/V5__Add_user_status.sql

-- Шаг 1: Добавить новую колонку с default значением
ALTER TABLE users ADD COLUMN status VARCHAR(50) DEFAULT 'ACTIVE';

-- Шаг 2: Создать индекс для производительности
CREATE INDEX idx_users_status ON users(status);

-- Шаг 3: Добавить constraint если нужно
ALTER TABLE users ADD CONSTRAINT chk_status 
  CHECK (status IN ('ACTIVE', 'INACTIVE', 'BANNED'));

✅ ШАГ 3: Тестирование

@SpringBootTest
@Transactional
class MigrationTest {
    
    @Autowired
    private JdbcTemplate jdbc;
    
    @Test
    void shouldMigrateSuccessfully() {
        // Проверить что таблица существует
        Integer columnCount = jdbc.queryForObject(
            "SELECT COUNT(*) FROM information_schema.COLUMNS " +
            "WHERE TABLE_NAME = 'users'",
            Integer.class
        );
        assertThat(columnCount).isGreaterThanOrEqualTo(5);
        
        // Проверить что new column существует
        Integer hasStatus = jdbc.queryForObject(
            "SELECT COUNT(*) FROM information_schema.COLUMNS " +
            "WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'status'",
            Integer.class
        );
        assertThat(hasStatus).isEqualTo(1);
    }
    
    @Test
    void shouldPreserveExistingData() {
        // Вставить данные до миграции (имитируем)
        User user = new User();
        user.setUsername("john");
        user.setEmail("john@example.com");
        // userRepository.save(user);
        
        // После миграции данные должны быть в целости
        // и иметь default значение для новой колонки
    }
}

✅ ШАГ 4: Deployment

1. Синхронизировать код с миграцией
2. Deploy приложения
3. Flyway/Liquibase автоматически выполнит миграцию
4. Приложение работает с new schema

Откат миграции

Flyway - нет встроенного отката

-- Нужно писать V_R__Rollback_migration.sql
-- Но это антипаттерн, лучше forward-only

Liquibase - есть встроенный откат

changeSet:
  id: 5
  author: developer
  changes:
    - addColumn:
        tableName: users
        columns:
          - column:
              name: status
              type: VARCHAR(50)
  rollback:
    - dropColumn:
        tableName: users
        columnName: status
# Откатить последнюю миграцию
liquibase rollback 1

Иерархия Entity ↔ Миграция

// 1. Меняешь Entity
@Entity
public class User {
    @Column(name = "department_id")
    private Long departmentId;
}

// 2. Создаешь миграцию
-- V6__Add_department_id.sql
ALTER TABLE users ADD COLUMN department_id BIGINT;
ALTER TABLE users ADD FOREIGN KEY (department_id) REFERENCES departments(id);

// 3. Запускаешь приложение
// Flyway выполнит миграцию при старте

// 4. В коде используешь
User user = new User();
user.setDepartmentId(1L);
userRepository.save(user);

Типичные ошибки

❌ Использовать update в production

# НИКОГДА не делай это!
spring.jpa.hibernate.ddl-auto=update

❌ Миграция без тестирования

Лучший способ потерять данные:
1. Написать миграцию не тестируя
2. Залить в production
3. "Ой, я забыл про cascade delete..."

❌ Миграция содержит hard-coded значения

-- ❌ ПЛОХО
UPDATE users SET status = 'ACTIVE' WHERE status IS NULL;

-- ✅ ХОРОШО (миграция + документация)
-- Установить default в новую колонку
ALTER TABLE users MODIFY COLUMN status VARCHAR(50) DEFAULT 'ACTIVE';

Рекомендуемый workflow

1. Разработка
   - Используй hibernate.ddl-auto=create для тестов
   - Entity first approach

2. Перед production
   - Выключи auto-generation (ddl-auto=validate)
   - Напиши SQL миграцию вручную
   - Protectioned данные
   - Протестируй миграцию

3. Deploy
   - Flyway/Liquibase автоматически применит миграцию
   - Нет ручных SQL скриптов

4. После deploy
   - Обнови Entity классы
   - Запусти приложение
   - Приложение валидирует что схема соответствует Entity

Выбор между Flyway и Liquibase

КритерийFlywayLiquibase
СинтаксисSQLYAML/XML/SQL
СложностьПрощеБольше возможностей
ОткатНет встроенногоЕсть встроенный
ИсторияВерсионированиеFull history
РекомендуетсяSimple projectsEnterprise

Итоговая best practice

✅ PRODUCTION:
- Используй Flyway или Liquibase
- Версионируй миграции как код
- Храни в git
- Каждая миграция = один файл
- Тестируй перед production
- Forward-only (не откатываешь в prod)

✅ DEVELOPMENT:
- Используй Entity first
- hibernate.ddl-auto=update
- Миграции пишешь только когда будешь готов к deployment

✅ ТЕСТИРОВАНИЕ:
- hibernate.ddl-auto=create для каждого теста
- Или TestContainers с реальной БД

Вывод: миграция БД в Hibernate - это critical процесс. Используй Flyway/Liquibase для production, Entity first для разработки, и всегда версионируй изменения.