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

Какие знаешь способы изменения таблиц?

2.3 Middle🔥 161 комментариев
#Основы Java

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

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

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

Способы изменения таблиц (миграции в SQL)

В production системах изменения схемы БД — это критичный процесс. Расскажу о подходах, которые использую для безопасного изменения таблиц.

1. Миграции с Flyway (SQL)

Flyway — это самый популярный инструмент для управления миграциями:

# application.yml
spring:
  flyway:
    locations: classpath:db/migration
    baseline-on-migrate: true
-- db/migration/V1__Create_users_table.sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(255) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL UNIQUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- db/migration/V2__Add_status_to_users.sql
ALTER TABLE users ADD COLUMN status VARCHAR(50) DEFAULT 'ACTIVE';

-- db/migration/V3__Create_index_on_email.sql
CREATE INDEX idx_users_email ON users(email);

Преимущества:

  • Простота и быстрота
  • Версионирование миграций
  • Автоматическая проверка целостности

Недостатки:

  • Нельзя откатить миграцию (только вперёд)
  • Требует тестирования на большых таблицах

2. Миграции с Liquibase (XML/YAML)

Liquibase — более гибкий инструмент:

<!-- db/changelog/001-create-users.xml -->
<databaseChangeLog>
    <changeSet id="1" author="john">
        <createTable tableName="users">
            <column name="id" type="BIGINT">
                <constraints primaryKey="true"/>
            </column>
            <column name="username" type="VARCHAR(255)">
                <constraints nullable="false" unique="true"/>
            </column>
            <column name="email" type="VARCHAR(255)">
                <constraints nullable="false" unique="true"/>
            </column>
        </createTable>
    </changeSet>
    
    <changeSet id="2" author="john">
        <addColumn tableName="users">
            <column name="status" type="VARCHAR(50)" defaultValue="ACTIVE"/>
        </addColumn>
    </changeSet>
</databaseChangeLog>

Преимущества:

  • Можно откатывать изменения
  • Версионирование в любом формате
  • Больше контроля

Недостатки:

  • Более сложная конфигурация
  • Медленнее Flyway

3. Alembic (Python для приложений на Python)

Если приложение на Python/SQLAlchemy:

alembic init migrations
alembic revision --autogenerate -m "Add users table"
alembic upgrade head
alembic downgrade -1  # Откатить последнюю миграцию

4. Online Schema Changes (OSC) - для больших таблиц

Для изменения структуры без блокировки:

-- Проблема: это заблокирует таблицу на время миграции
ALTER TABLE large_table ADD COLUMN new_column VARCHAR(255);

-- Решение 1: Добавить колонку, которую можно заполнять в фоне
ALTER TABLE large_table ADD COLUMN temp_column VARCHAR(255) DEFAULT 'default';

-- Затем в фоне скопируем данные
UPDATE large_table SET temp_column = old_column WHERE temp_column = 'default';

-- Потом переименуем
ALTER TABLE large_table DROP COLUMN old_column;
ALTER TABLE large_table RENAME COLUMN temp_column TO old_column;

Решение 2: Использовать GitHub's gh-ost (online schema migration):

gh-ost \
  --host=192.168.1.100 \
  --user=root \
  --password=secret \
  --database=mydb \
  --table=large_table \
  --alter="ADD COLUMN new_column VARCHAR(255)" \
  --execute

gh-ost создаёт новую таблицу, копирует данные в фоне, переключает триггеры без блокировки.

5. Zero-downtime deployments (Canary)

Стратегия для безопасного развертывания:

// Шаг 1: Добавить новую колонку (не использовать)
ALTER TABLE users ADD COLUMN middle_name VARCHAR(255) NULL;

// Шаг 2: Развернуть новый код, который пишет в обе колонки
@Entity
public class User {
    @Column(name = "full_name")
    private String fullName;
    
    @Column(name = "middle_name")
    private String middleName;
    
    // Сеттер пишет в обе колонки
    public void setFullName(String fullName) {
        this.fullName = fullName;
        // Извлекаем middle name
        this.middleName = extractMiddleName(fullName);
    }
}

// Шаг 3: После того как все инстансы обновлены, удалить старую колонку
ALTER TABLE users DROP COLUMN full_name;

6. Обратная совместимость при миграциях

Всегда пишите код так, чтобы он работал с обеими версиями схемы:

@Entity
@Table(name = "products")
public class Product {
    @Id
    private Long id;
    
    // Старое поле (будет удалено в миграции v3)
    @Column(name = "price_cents", nullable = true)
    private Long priceCents;
    
    // Новое поле
    @Column(name = "price_major", nullable = true)
    private Long priceMajor;
    
    // Геттер работает с обоими полями
    public Long getPrice() {
        if (priceMajor != null) {
            return priceMajor;
        }
        return priceCents / 100;  // Fallback на старое поле
    }
    
    // Сеттер пишет в оба поля
    public void setPrice(Long price) {
        this.priceMajor = price;
        this.priceCents = price * 100;
    }
}

7. Expand-Contract паттерн

Безопасное переименование колонок:

-- Миграция 1: Expand - добавляем новую колонку
ALTER TABLE users ADD COLUMN email_address VARCHAR(255);
CREATE UNIQUE INDEX idx_users_email_address ON users(email_address);

-- Миграция 2: Populating (может быть долгой)
UPDATE users SET email_address = email WHERE email_address IS NULL;

-- Миграция 3: Dual-write - пишем в обе колонки
-- Код в приложении пишет в email и email_address

-- Миграция 4: Dual-read - читаем из новой колонки
-- Код читает из email_address, fallback на email

-- Миграция 5: Contract - удаляем старую колонку
ALTER TABLE users DROP COLUMN email;
ALTER TABLE users RENAME COLUMN email_address TO email;

8. Problematic миграции и их решения

Проблема: Удаление NOT NULL колонки

-- ОПАСНО - может привести к потере данных
ALTER TABLE users DROP COLUMN email;

-- ПРАВИЛЬНО
ALTER TABLE users ALTER COLUMN email DROP NOT NULL;  -- Сначала сделать опциональной
ALTER TABLE users DROP COLUMN email;  -- Потом удалить

Проблема: Добавление колонки с DEFAULT на большой таблице

-- МЕДЛЕННО - переписывает всю таблицу
ALTER TABLE users ADD COLUMN status VARCHAR(50) DEFAULT 'ACTIVE' NOT NULL;

-- БЫСТРЕЕ
ALTER TABLE users ADD COLUMN status VARCHAR(50) DEFAULT 'ACTIVE';  // Nullable сначала
UPDATE users SET status = 'ACTIVE';  // В фоне
ALTER TABLE users ALTER COLUMN status SET NOT NULL;  // Потом ограничение

Проблема: Изменение типа данных

-- МОЖЕТ БЫТЬ ОПАСНО
ALTER TABLE users ALTER COLUMN age TYPE BIGINT;

-- БЕЗОПАСНЕЕ через промежуточную колонку
ALTER TABLE users ADD COLUMN age_new BIGINT;
UPDATE users SET age_new = age::BIGINT;
ALTER TABLE users DROP COLUMN age;
ALTER TABLE users RENAME COLUMN age_new TO age;

9. Тестирование миграций

@DataJpaTest
public class MigrationTest {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Test
    public void testMigrationV2_AddStatusToUsers() {
        // Проверяем, что колонка добавлена
        String query = "SELECT column_name FROM information_schema.columns " +
                      "WHERE table_name = 'users' AND column_name = 'status'";
        
        List<Map<String, Object>> result = jdbcTemplate.queryForList(query);
        assertTrue(result.size() > 0, "Status column should exist");
    }
    
    @Test
    public void testMigrationV3_CreateIndexOnEmail() {
        String query = "SELECT indexname FROM pg_indexes WHERE tablename = 'users'";
        List<Map<String, Object>> result = jdbcTemplate.queryForList(query);
        
        boolean hasEmailIndex = result.stream()
            .anyMatch(r -> r.get("indexname").toString().contains("email"));
        assertTrue(hasEmailIndex, "Email index should exist");
    }
}

10. Best Practices для миграций

  • Один файл — одна миграция (логическое изменение)
  • Тестируй на staging перед production
  • Всегда делай бэкап перед большой миграцией
  • Мониторь performance во время миграции
  • Используй transaction_isolation при needed
  • Версионируй миграции (V1, V2...)
  • Не используй DROP TABLE без очень веских причин
  • Документируй причину миграции в комментариях
  • Откатывай стратегию — знай, как откатить изменение
  • Параллельные миграции опасны — запускай одну за раз

В целом: миграции БД требуют тщательного планирования. Всегда думай о backwards compatibility, performance impact и плане отката.