JavaRush /Курсы /Spring Data JPA /@Column: контракт ко...

@Column: контракт колонки

Spring Data JPA
5 уровень , 2 лекция
Открыта

1. @Column как контракт

Когда начинаешь писать первую entity, очень хочется думать так: «Ну поле String code — оно же и так строка. Зачем что-то указывать?». Мысль нормальная, особенно если вы только что победили @Entity и @Id и чувствуете, что уже заслужили чай с печеньками. Но @Column — не про «красиво», а про фиксирование правил хранения данных: как поле называется в таблице, может ли быть NULL, должно ли быть уникальным, какой длины ожидается строка и с какой точностью хранить числа вроде цены.

Важно уловить смысл @Column: это не «приказ Hibernate», а ваш контракт между Java-кодом и SQL-схемой. Даже если вы сейчас используете авто-генерацию схемы, в реальности база всё равно будет жить по этим правилам. Если контракт туманный, туман придёт и в данные.

Мини-пример: аннотировать всё подряд не обязательно, но ключевые поля обычно стоит.

import jakarta.persistence.Column;

public class Category {

    // Ключевое поле домена: фиксируем обязательность, уникальность и максимальную длину
    @Column(nullable = false, unique = true, length = 50)
    private String code;
}

Здесь в одной строке мы сказали о поле гораздо больше, чем «это строка»: оно обязательное, уникальное и с разумным ограничением по длине.

2. Имя колонки: @Column(name = ...)

Имена — отдельная дисциплина. В Java вы обычно пишете camelCase, в SQL чаще встречается snake_case, а ещё бывают legacy-схемы с именами вроде CAT_CD — и это не котик, а «category code». В мире JPA есть дефолтные правила, которые пытаются угадать имя колонки по имени поля. Иногда всё совпадает идеально, а иногда вы смотрите на SQL-лог и думаете: «Это точно моё приложение, а не чужое?»

Параметр name в @Column — это способ сказать: «перестань угадывать, вот точное имя». И это полезно не только для красоты, но и как страховка от рефакторинга: вы можете переименовать Java-поле, но оставить имя колонки прежним, и схема базы не начнёт «самопроизвольно меняться».

Простой пример, когда Java-имя и SQL-имя отличаются:

import jakarta.persistence.Column;

public class Category {

    // Явно фиксируем имя в БД: рефакторинг поля в Java не должен "случайно" переименовать колонку
    @Column(name = "category_code", nullable = false, unique = true, length = 50)
    private String code;
}

Когда name нужно, а когда хватит дефолтов

На старте легко скатиться в крайности: либо «аннотировать всё», либо «не аннотировать ничего». Оба подхода быстро приводят к боли, просто по-разному. Если ничего не фиксировать, вы становитесь заложником naming strategy и случайных переименований. Если фиксировать всё подряд без смысла, код начинает напоминать дипломную работу по аннотациям.

Здоровая логика такая: если поле — важная часть контракта (код категории, SKU товара, цена, статус), лучше назвать колонку явно или хотя бы явно задать ограничения. Для второстепенных полей, которые не участвуют в уникальности, не имеют строгих размеров и не критичны для контракта, иногда достаточно дефолтов. Но как только вы ловите себя на мысли «это поле очень важное», — это и есть момент для @Column(...).

Небольшой пример, где имя можно оставить дефолтным, но ограничения всё равно закрепить:

import jakarta.persistence.Column;

public class Category {

    // Имя колонки может остаться дефолтным, но длину и обязательность лучше закрепить контрактом
    @Column(nullable = false, length = 100)
    private String name;
}

Явное имя и защита от SQL-слов

Есть и чисто человеческий фактор: мы называем поля так, как удобно нам, а SQL любит «зарезервированные слова». Например, назвать колонку order или user — почти как оставить банановую кожуру на лестнице: кто-нибудь обязательно поскользнётся. Если вы заранее фиксируете @Column(name = "customer_order") или @Table(name = "customer_order"), шанс столкновения с SQL-синтаксисом заметно ниже.

Сейчас мы не строим сложную схему и не подбираем идеальную нотацию имён на уровне «как в FAANG», но мысль простая: имя в SQL — это часть контракта, и name = "..." позволяет сделать этот контракт явным, а не гадательным.

3. nullable: обязательность и NULL

nullable кажется элементарным: nullable = false — поле обязательно, nullable = true — нет. Но именно здесь начинаются первые настоящие грабли. В Java null означает «значения нет», а в SQL NULL — это отдельное состояние колонки. Миры похожи, но не идентичны: Java может молча позволить вам забыть присвоить значение, а база честно откажет во вставке и скажет: «Вы обещали, что здесь всегда будет значение».

Поэтому @Column(nullable = false) — это способ закрепить, что поле не просто «желательно», а обязательная часть строки. И если вы попытаетесь сохранить сущность без этого значения, получите ошибку — и это хорошо: лучше сразу увидеть проблему, чем месяцами хранить мусорные данные.

Пример на нашем домене: код категории должен быть всегда.

import jakarta.persistence.Column;

public class Category {

    // Если поле обязано быть заполнено по смыслу домена — пусть это будет NOT NULL в БД
    @Column(nullable = false, unique = true, length = 50)
    private String code;
}

Как NULL попадает в базу

Частый сценарий новичка: вы создали объект new Category(), заполнили пару полей, а одно забыли. В Java это не выглядит преступлением века — компилятор молчит. Но при сохранении в БД оказывается, что «забыл заполнить» и «хочу хранить NULL» для базы почти одно и то же.

Если колонка «NOT NULL», база откажется. Если колонка допускает NULL, база запишет NULL, и вы потом будете разбираться, почему в отчёте по категориям внезапно есть категория без имени. И да, это обычно происходит в пятницу вечером.

Небольшой пример DDL, который может получиться из nullable = false:

create table category (
    id bigint not null,
    code varchar(50) not null, -- код обязателен: без него строка "невалидна" по контракту
    name varchar(100) not null -- имя тоже обязательное: это NOT NULL на уровне схемы
);

Даже если DDL пока не стало «родным», ключевое слово тут видно сразу: not null. Это и есть обязательность.

Нюанс Java-типов: примитивы и null

Здесь появляется «маленькая подлость» Java, о которой полезно помнить. Если поле типа boolean, то null оно не умеет по определению. У него всегда будет значение: false по умолчанию. А вот Boolean уже может быть null. То же самое с int/Integer, long/Long и так далее.

Это напрямую связано с nullable. Если вы хотите, чтобы колонка могла быть NULL, логично выбирать тип, который это допускает: Boolean, Integer. Если поле должно быть обязательным, примитив может быть удобен — но только если значение по умолчанию (false, 0) действительно имеет смысл для домена.

Мы не превращаем эту лекцию в отдельный урок по выбору Java-типов, но важно увидеть связь: nullable — это не только про SQL, это ещё и про то, что вы реально сможете хранить в Java-поле.

4. unique: уникальность как правило

unique = true новички часто воспринимают как «ну пусть будет, хуже не станет». На практике хуже как раз становится, если включать уникальность бездумно: вы легко запретите вполне легальные сценарии данных. Но если уникальность действительно является бизнес-правилом — например, для sku товара или code категории, — тогда unique превращает базу в надёжного охранника: дубликаты не пройдут, даже если кто-то ошибся в коде.

Для нашего mini-shop это очень естественно: у категории есть код, и он не должен повторяться. У товара есть sku, и он тоже не должен повторяться. Если допустить дубликаты, вы потом не сможете понять, «какой именно товар» продаёте, даже если очень попросите.

Пример:

import jakarta.persistence.Column;

public class Product {

    // SKU — идентификатор товара для внешнего мира, дубликаты недопустимы
    @Column(nullable = false, unique = true, length = 64)
    private String sku;
}

Что происходит в базе при unique = true

Важно понимать это приземлённо: unique = true — не «проверка в Java», а просьба создать уникальное ограничение на уровне базы данных, если схема генерируется. В SQL-мире это обычно выглядит как UNIQUE-constraint, и часто вместе с ним автоматически создаётся индекс, чтобы проверка уникальности была быстрой.

Примерный DDL-фрагмент:

create table product (
    id bigint not null,
    sku varchar(64) not null,
    constraint uk_product_sku unique (sku) -- база не пропустит два одинаковых sku
);

И вот здесь начинается честная инженерия: база не даст вставить второй товар с тем же sku. Да, вы получите ошибку. Но эта ошибка — не зло, а сигнал: вы нарушили инвариант данных.

unique = true — только для одной колонки

Ещё один нюанс, который полезно знать, чтобы не удивляться. @Column(unique = true) хорошо подходит, когда уникальным должен быть один столбец. Если бизнес-правило звучит как «уникальна пара значений» — например, сочетание country + documentNumber, — это уже другая форма ограничения и решается она иначе.

В рамках наших первых сущностей нам это не нужно, поэтому мы держимся простого и понятного: уникальный code и уникальный sku. Но важно помнить границу: unique = true — не универсальная кнопка для всех видов уникальности.

5. length: размеры строк

Параметр length кажется мелочью ровно до того момента, пока вы не увидите в базе «код категории» на 10 000 символов или «имя товара», в которое кто-то случайно вставил целую статью из Википедии. Да, звучит как анекдот, но такие вещи реально происходят, когда вы не ограничиваете размеры и не фиксируете контракт хранения.

По умолчанию строковые колонки часто получаются varchar(255). Это исторически популярное значение, но оно не обязано совпадать с вашим доменом. Код категории обычно маленький: 20–50 символов. Название товара — допустим, до 120. Описание категории может быть длиннее, но всё равно в пределах разумного.

Пример:

import jakarta.persistence.Column;

public class Category {

    // Короткий "код" — значит короткая колонка: фиксируем верхнюю границу явно
    @Column(nullable = false, unique = true, length = 50)
    private String code;

    // Название длиннее кода, но тоже с разумным пределом
    @Column(nullable = false, length = 100)
    private String name;
}

length как часть договорённости

Можно подумать: «Ну ограничим длину — это же про производительность». В реальности, особенно в PostgreSQL, длина varchar чаще не про скорость, а про дисциплину данных. Вы говорите базе и команде: «Это поле для короткого кода. Если вы пытаетесь запихнуть туда роман Толстого — вы что-то делаете не так».

И есть ещё практический бонус: когда вы смотрите на схему таблицы или на generated DDL, varchar(50) сразу читается как «код», а varchar(500) — как «описание». Это делает схему понятнее даже без документации. Да, звучит скучно, но в реальном проекте скучное обычно означает «надёжное».

Длинные тексты: без length = 5000 по привычке

Если текст действительно большой — например, описание товара на несколько страниц, — обычно используют другие подходы хранения. В JPA для этого есть отдельные механизмы, и они немного отличаются от обычного varchar. Но для нашего базового старта это не нужно: мы делаем маленькие, понятные сущности, и для description нам достаточно условных 500 символов.

Пример:

import jakarta.persistence.Column;

public class Category {

    // Описание опционально: nullable по умолчанию true, но размер всё равно ограничиваем
    @Column(length = 500)
    private String description;
}

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

6. precision и scale: числа и деньги

Если строки — это место, где чаще всего забывают про контракт, то числа — это место, где люди чаще всего обманывают себя. Особенно деньги. Новичок часто хочет хранить цену как double, потому что «это же число с точкой». А потом видит 19.99999999997 и понимает, что компьютер умеет быть креативным.

Для денег в Java обычно используют BigDecimal, а в базе — тип вроде numeric/decimal. И вот тут в игру входят precision и scale. precision — это общее количество цифр, scale — количество цифр после запятой. Для обычных цен сочетание 12,2 — частый и понятный выбор: до 10 цифр до запятой и 2 после.

Пример маппинга цены:

import jakarta.persistence.Column;
import java.math.BigDecimal;

public class Product {

    // Деньги храним предсказуемо: 2 знака после запятой — часть контракта хранения
    @Column(nullable = false, precision = 12, scale = 2)
    private BigDecimal price;
}

numeric(12,2) на практике

Представьте цену 9999999999.99. Это 10 цифр до запятой и 2 после — всего 12 цифр. Она поместится в numeric(12,2). А вот 99999999999.99 уже имеет 11 цифр до запятой — всего 13 цифр — и не поместится.

Это не математика ради математики. Это способ заранее договориться, какие цены допустимы в вашем домене. В mini-shop мы не продаём планеты, поэтому precision = 12 выглядит разумно.

Примерный DDL-фрагмент:

price numeric(12, 2) not null

BigDecimal: строка против double

Это уже почти Java Core, но слишком полезно, чтобы промолчать. BigDecimal можно создавать из строки и из double. Из строки — предсказуемо. Из double — иногда с сюрпризами, потому что double хранит число в двоичном виде, и не все десятичные дроби представляются там точно.

import java.math.BigDecimal;

// Корректно для денег: создаём BigDecimal из строки, без двоичных "хвостов"
BigDecimal ok = new BigDecimal("49.90");

// Опасно для денег: из double можно получить неожиданные десятичные артефакты
BigDecimal weird = new BigDecimal(49.90);

System.out.println(ok);     // 49.90
System.out.println(weird);  // 49.899999999999998578914... (примерно)

Смысл не в том, чтобы запомнить «магическую строку», а в том, что деньги любят точность. А precision/scale в @Column делают точность частью SQL-контракта, чтобы вы случайно не начали хранить «стоимость товара» с 17 знаками после запятой.

7. @Column в генерируемом SQL

Сейчас самый приятный момент: мы уже связали параметры @Column с понятными SQL-идеями (NOT NULL, UNIQUE, varchar(n), numeric(p,s)). Но всё это остаётся теорией, пока вы не увидели её в генерируемом SQL. Hibernate и JPA не телепаты: они не читают ваши мысли о схеме. Они читают mapping и генерируют SQL под него — либо DDL для создания таблиц, если включена автогенерация, либо DML для вставки и обновления данных.

Поэтому полезно приучить себя к простой мысли: @Column вы пишете не для того, чтобы «аннотация была», а чтобы потом открыть SQL-лог и увидеть, что действительно происходит. Это как с рецептом: можно долго читать, а можно один раз приготовить.

Небольшая схема того, что происходит концептуально:

flowchart TD
    A["Java-поле
private String code"] --> B["@Column(nullable=false, unique=true, length=50)"] B --> C["SQL-колонка
code varchar(50) not null"] C --> D["Ограничение
unique(code)"]

И пример DDL, который может появиться в логах при старте приложения:

create table category (
    id bigint not null,
    code varchar(50) not null,
    name varchar(100) not null,
    description varchar(500),
    active boolean not null,
    primary key (id),
    constraint uk_category_code unique (code)
);

Да, реальный вывод зависит от настроек генерации схемы, стратегии именования и версии Hibernate. Но смысл остаётся железным: @Column — это то место, где вы напрямую влияете на такие фразы, как not null, unique и размеры типов.

8. Типичные ошибки при работе с @Column

Ошибки в @Column неприятны тем, что выглядят мелочами, а стреляют либо в рантайме при сохранении данных, либо — хуже — тихо создают кривые данные. Новички страдают от этого особенно часто, потому что в коде всё может выглядеть прилично: Java-класс компилируется, приложение запускается, а проблема всплывает только в первом реальном сценарии. Поэтому лучше заранее знать, где лежат эти грабли.

Ошибка №1: воспринимать @Column как декоративную аннотацию и ничего не фиксировать для важных полей.
Когда code, sku или price остаются без явных ограничений, вы по сути говорите: «пусть будет как получится». Потом оказывается, что в базе у вас varchar(255) для кода, NULL там, где значения обязаны быть, и неожиданные дубликаты. Это не «ошибка Hibernate», а отсутствие контракта.

Ошибка №2: ставить unique = true «на всякий случай».
Уникальность — это не стиль, а бизнес-правило. Если вы пометили name категории как уникальное, а потом захотели две категории «Books» в разных языках или контекстах, база справедливо скажет «нельзя». Если правило не железное, уникальности не место в схеме.

Ошибка №3: забывать про nullable = false, а потом удивляться NULL в обязательных полях.
Если поле по смыслу обязательно, но вы не закрепили это в контракте, база будет хранить «отсутствие значения» как нормальное состояние. А потом вы начинаете писать защитный код во всех местах чтения: if (name == null) .... Это как купить зонт после того, как вы уже промокли до нитки.

Ошибка №4: не задавать precision и scale для BigDecimal, особенно для денег.
Без явного precision/scale вы рискуете получить непредсказуемое хранение чисел: где-то округление, где-то лишние знаки, где-то разные типы в разных базах. Деньги и количества требуют дисциплины. Если вы храните цену, объявите её формат как часть контракта.

Ошибка №5: оставлять строки «по умолчанию» там, где размер очевиден.
Код категории почти никогда не должен быть varchar(255). SKU товара — тоже. Если размер известен, length делает данные аккуратнее и схему понятнее. А ещё это помогает ловить ошибки раньше: когда приложение попыталось записать слишком длинный код, вы узнаете об этом сразу, а не спустя неделю.

1
Задача
Spring Data JPA, 5 уровень, 2 лекция
Недоступна
Контракт колонки `brand_code`
Контракт колонки `brand_code`
1
Задача
Spring Data JPA, 5 уровень, 2 лекция
Недоступна
Точность и масштаб для `discount_amount`
Точность и масштаб для `discount_amount`
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ