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

Как добавить поле в сущность в Hibernate

1.2 Junior🔥 111 комментариев
#ORM и Hibernate#Базы данных и SQL

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

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

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

Ответ

Добавление поля в сущность Hibernate: полный процесс

Добавление нового столбца в БД и соответствующего поля в Hibernate сущность требует синхронизации на нескольких уровнях. Расскажу о проверенном подходе.

Шаг 1: Добавляем столбец в БД через миграцию

-- V001__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- V002__add_phone_field.sql (позже добавляем новое поле)
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

Шаг 2: Обновляем Hibernate сущность

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

@Entity
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "name", nullable = false)
    private String name;
    
    @Column(name = "email", unique = true, nullable = false)
    private String email;
    
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    // ★ НОВОЕ ПОЛЕ
    @Column(name = "phone", length = 20, nullable = true)
    private String phone;
    
    // Конструкторы
    public User() {}
    
    public User(String name, String email) {
        this.name = name;
        this.email = email;
        this.createdAt = LocalDateTime.now(ZoneId.of("UTC"));
    }
    
    // Getters и Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
    
    // ★ Getter и Setter для нового поля
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }
    
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", phone='" + phone + '\'' +
                ", createdAt=" + createdAt +
                '}';
    }
}

Шаг 3: Используем поле в репозитории

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // Поиск по email (существующее поле)
    Optional<User> findByEmail(String email);
    
    // ★ Поиск по новому полю (phone)
    Optional<User> findByPhone(String phone);
    
    // ★ Поиск по обоим полям
    List<User> findByNameAndPhone(String name, String phone);
    
    // ★ Кастомный запрос с новым полем
    @Query("SELECT u FROM User u WHERE u.phone IS NOT NULL")
    List<User> findUsersWithPhone();
    
    // ★ Обновление нового поля
    @Query("UPDATE User u SET u.phone = :phone WHERE u.id = :id")
    void updatePhone(Long id, String phone);
}

Шаг 4: Используем в Service слое

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {
    
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    // Получить пользователя с новым полем
    public User getUserWithPhone(Long userId) {
        return userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException("User not found"));
    }
    
    // ★ Использовать новое поле
    @Transactional
    public void addPhoneToUser(Long userId, String phone) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException("User not found"));
        
        // Валидация
        if (!isValidPhoneNumber(phone)) {
            throw new IllegalArgumentException("Invalid phone format");
        }
        
        // Устанавливаем поле
        user.setPhone(phone);
        
        // Сохраняем (Hibernate обнаружит изменение и выполнит UPDATE)
        userRepository.save(user);
    }
    
    // Поиск по телефону
    @Transactional(readOnly = true)
    public User findByPhone(String phone) {
        return userRepository.findByPhone(phone)
            .orElseThrow(() -> new UserNotFoundException("User with phone " + phone + " not found"));
    }
    
    // Получить всех пользователей с телефоном
    @Transactional(readOnly = true)
    public List<User> getUsersWithPhone() {
        return userRepository.findUsersWithPhone();
    }
    
    private boolean isValidPhoneNumber(String phone) {
        // Простая валидация
        return phone != null && phone.matches("\\d{10,20}");
    }
}

Шаг 5: DTO для передачи данных

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
    private Long id;
    private String name;
    private String email;
    private String phone;  // ★ Новое поле
    private LocalDateTime createdAt;
    
    // Конвертер из сущности в DTO
    public static UserDto fromEntity(User user) {
        return new UserDto(
            user.getId(),
            user.getName(),
            user.getEmail(),
            user.getPhone(),  // ★ Новое поле
            user.getCreatedAt()
        );
    }
    
    // Конвертер из DTO в сущность
    public User toEntity() {
        User user = new User();
        user.setId(this.id);
        user.setName(this.name);
        user.setEmail(this.email);
        user.setPhone(this.phone);  // ★ Новое поле
        user.setCreatedAt(this.createdAt);
        return user;
    }
}

Шаг 6: API контроллер

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
        User user = userService.getUserWithPhone(id);
        return ResponseEntity.ok(UserDto.fromEntity(user));
    }
    
    // ★ Новый endpoint для установки телефона
    @PutMapping("/{id}/phone")
    public ResponseEntity<UserDto> updatePhone(
            @PathVariable Long id,
            @RequestBody PhoneUpdateRequest request) {
        userService.addPhoneToUser(id, request.getPhone());
        User user = userService.getUserWithPhone(id);
        return ResponseEntity.ok(UserDto.fromEntity(user));
    }
    
    // ★ Поиск по телефону
    @GetMapping("/by-phone/{phone}")
    public ResponseEntity<UserDto> getUserByPhone(@PathVariable String phone) {
        User user = userService.findByPhone(phone);
        return ResponseEntity.ok(UserDto.fromEntity(user));
    }
    
    // ★ Получить всех пользователей с телефоном
    @GetMapping("/with-phone")
    public ResponseEntity<List<UserDto>> getUsersWithPhone() {
        List<UserDto> users = userService.getUsersWithPhone()
            .stream()
            .map(UserDto::fromEntity)
            .toList();
        return ResponseEntity.ok(users);
    }
}

public class PhoneUpdateRequest {
    private String phone;
    
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }
}

Шаг 7: Тесты

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.TestPropertySource;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@TestPropertySource("classpath:application-test.properties")
public class UserRepositoryTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    public void testAddPhoneToUser() {
        // Arrange
        User user = new User("John Doe", "john@example.com");
        user.setPhone("1234567890");  // ★ Используем новое поле
        
        // Act
        User saved = userRepository.save(user);
        
        // Assert
        assertThat(saved.getId()).isNotNull();
        assertThat(saved.getPhone()).isEqualTo("1234567890");  // ★ Проверяем
    }
    
    @Test
    public void testFindByPhone() {
        // Arrange
        User user = new User("Jane Doe", "jane@example.com");
        user.setPhone("9876543210");
        userRepository.save(user);
        
        // Act
        User found = userRepository.findByPhone("9876543210").orElse(null);
        
        // Assert
        assertThat(found).isNotNull();
        assertThat(found.getName()).isEqualTo("Jane Doe");
        assertThat(found.getPhone()).isEqualTo("9876543210");
    }
    
    @Test
    public void testFindUsersWithPhone() {
        // Arrange
        User user1 = new User("User1", "user1@example.com");
        user1.setPhone("1111111111");
        User user2 = new User("User2", "user2@example.com");
        // user2 без телефона
        
        userRepository.saveAll(List.of(user1, user2));
        
        // Act
        List<User> usersWithPhone = userRepository.findUsersWithPhone();
        
        // Assert
        assertThat(usersWithPhone).hasSize(1);
        assertThat(usersWithPhone.get(0).getPhone()).isNotNull();
    }
}

Чек-лист для добавления поля

✓ 1. Миграция БД
   ↳ ALTER TABLE add column
   ↳ Прогнать миграцию на dev

✓ 2. Hibernate Entity
   ↳ @Column аннотация
   ↳ Getter и Setter
   ↳ toString() обновлен
   ↳ equals() и hashCode() если нужны

✓ 3. Repository
   ↳ Новые методы поиска (findByPhone)
   ↳ Кастомные запросы (@Query)

✓ 4. Service
   ↳ Валидация данных
   ↳ Бизнес логика
   ↳ @Transactional если нужно

✓ 5. DTO
   ↳ Новое поле в DTO
   ↳ Маппинг entity ↔ DTO

✓ 6. API Controller
   ↳ Новые endpoints
   ↳ Валидация input'ов
   ↳ Документация (Swagger)

✓ 7. Тесты
   ↳ Unit тесты
   ↳ Integration тесты
   ↳ E2E тесты

Частые ошибки

// ✗ НЕПРАВИЛЬНО: забыли @Column
@Entity
public class User {
    private String phone;  // Hibernate не знает, какой столбец использовать!
}

// ✓ ПРАВИЛЬНО: явно указываем столбец
@Entity
public class User {
    @Column(name = "phone", nullable = true)
    private String phone;  // Hibernate знает, что это столбец phone
}

// ✗ НЕПРАВИЛЬНО: забыли миграцию
// Добавили поле в сущность, но БД не обновлена!

// ✓ ПРАВИЛЬНО: сначала миграция, потом код
// 1. Миграция ALTER TABLE
// 2. Обновить Entity
// 3. Обновить Repository
// 4. Обновить Service, DTO, Controller

Вывод

Для добавления поля в Hibernate сущность:

  1. Миграция БД первой — ALTER TABLE добавить столбец
  2. Entity — @Column, getter, setter
  3. Repository — новые методы поиска
  4. Service — бизнес логика и валидация
  5. DTO — для API контрактов
  6. Controller — новые endpoints
  7. Тесты — unit + integration

Синхронизация между БД и кодом критична. Помните: Source of Truth — миграции, Hibernate модели — просто описание.