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 делает данные аккуратнее и схему понятнее. А ещё это помогает ловить ошибки раньше: когда приложение попыталось записать слишком длинный код, вы узнаете об этом сразу, а не спустя неделю.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ