УРОК 6 / 30 0%

Spring Data JPA

🎯

Цель урока

Сделать полноценный CRUD для задач с правильными HTTP-статусами (201/204/404), обработкой ошибок, и подготовить Task к подключению JPA.

🧠

Теория · для собеса

1 JPA vs JDBC

| JDBC | JPA |

|---|---|

| Пишешь SQL вручную | Пишешь методы, SQL генерирует Hibernate |

| RowMapper для каждого запроса | findById(42)Optional<Task> |

| Сам управляешь Connection | Сам управляешь EntityManager (через @Transactional) |

| Сырой SQL для сложных запросов | JPQL/HQL или Native Query |

2 Что такое JPA

JPA (Jakarta Persistence API) — это спецификация (интерфейсы + аннотации).

Hibernate — самая популярная реализация JPA. Spring Boot по умолчанию использует именно её.

3 Что такое Entity

Entity — это Java-класс, который JPA маппит на таблицу БД.


@Entity                    // это JPA-сущность
@Table(name = "tasks")     // имя таблицы
public class Task {
    @Id                    // PRIMARY KEY
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // SERIAL в PostgreSQL
    private Long id;

    @Column(nullable = false, length = 200)  // VARCHAR(200) NOT NULL
    private String title;
    // ...
}
4 Что делает JpaRepository

public interface TaskRepository extends JpaRepository {
    // УЖЕ доступны из коробки:
    // findById(Long id) → Optional
    // findAll() → List
    // save(Task) → Task
    // deleteById(Long id)
    // count() → long
    // existsById(Long id) → boolean
    // ...и ещё ~30 методов
}
5 @Transactional — зачем

JPA работает через EntityManager, который привязан к транзакции. Без @Transactional:

  • findById работает (транзакция создаётся автоматически для read)
  • save бросит TransactionRequiredException

Решение: на сервисных методах, которые пишут в БД, ставь @Transactional.

⚠️ Важно: @Transactional работает через AOP-прокси. Вызов из того же класса НЕ активирует транзакцию (урок 14, Spilca объясняет это).

---

💻

Практика: улучшаем CRUD

  1. 1
    Зависимости
    
    dependencies {
        implementation("org.springframework.boot:spring-boot-starter-data-jpa")
        runtimeOnly("org.postgresql:postgresql")
    }
    

    ⚠️ Убираем spring-boot-starter-jdbc — он не нужен, JPA приносит свой DataSource.

  2. 2
    model/Task.java (теперь JPA-Entity)
    
    package com.taskflow.model;
    
    import jakarta.persistence.*;
    import java.time.Instant;
    import java.util.Objects;
    
    @Entity
    @Table(name = "tasks")
    public class Task {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(nullable = false, length = 200)
        private String title;
    
        @Column(columnDefinition = "TEXT")
        private String description;
    
        @Column(nullable = false)
        private boolean done = false;
    
        @Column(name = "created_at", nullable = false, updatable = false)
        private Instant createdAt;
    
        @Column(name = "updated_at", nullable = false)
        private Instant updatedAt;
    
        public Task() {}
    
        public Task(String title, String description) {
            this.title = title;
            this.description = description;
        }
    
        @PrePersist
        void onCreate() {
            Instant now = Instant.now();
            this.createdAt = now;
            this.updatedAt = now;
        }
    
        @PreUpdate
        void onUpdate() {
            this.updatedAt = Instant.now();
        }
    
        // Getters / Setters
        public Long getId() { return id; }
        public void setId(Long id) { this.id = id; }
        public String getTitle() { return title; }
        public void setTitle(String title) { this.title = title; }
        public String getDescription() { return description; }
        public void setDescription(String description) { this.description = description; }
        public boolean isDone() { return done; }
        public void setDone(boolean done) { this.done = done; }
        public Instant getCreatedAt() { return createdAt; }
        public Instant getUpdatedAt() { return updatedAt; }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Task task)) return false;
            return Objects.equals(id, task.id);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(id);
        }
    }
    

    Ключевые моменты:

    • @PrePersist / @PreUpdate — JPA-колбэки, вызываются перед INSERT/UPDATE
    • equals/hashCode по id (теперь id всегда есть после persist)
    • updatable = false на createdAt — нельзя изменить после создания
    • private поля + геттеры/сеттеры (стандарт для JPA, не для record)
  3. 3
    repository/TaskRepository.java (JPA-версия)
    
    package com.taskflow.repository;
    
    import com.taskflow.model.Task;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    
    import java.util.List;
    
    @Repository
    public interface TaskRepository extends JpaRepository {
        List findByDone(boolean done);
        List findByTitleContainingIgnoreCase(String title);
    }
    

    Магия Spring Data JPA: ты пишешь имя метода → Spring парсит → генерирует SQL.

    • findByDoneSELECT * FROM tasks WHERE done = ?
    • findByTitleContainingIgnoreCaseSELECT * FROM tasks WHERE LOWER(title) LIKE LOWER('%' || ? || '%')

    Поддерживаемые ключевые слова: findBy, countBy, deleteBy, And, Or, Containing, IgnoreCase, OrderBy, Top, Distinct, Between, LessThan, GreaterThan, IsNull, In, NotIn...

  4. 4
    service/TaskService.java
    
    package com.taskflow.service;
    
    import com.taskflow.model.Task;
    import com.taskflow.repository.TaskRepository;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.List;
    import java.util.Optional;
    
    @Service
    public class TaskService {
        private final TaskRepository repo;
    
        public TaskService(TaskRepository repo) {
            this.repo = repo;
        }
    
        public List findAll() { return repo.findAll(); }
        public List findByDone(boolean done) { return repo.findByDone(done); }
        public Optional findById(Long id) { return repo.findById(id); }
    
        @Transactional
        public Task create(Task task) { return repo.save(task); }
    
        @Transactional
        public Optional update(Long id, Task task) {
            return repo.findById(id).map(existing -> {
                existing.setTitle(task.getTitle());
                existing.setDescription(task.getDescription());
                existing.setDone(task.isDone());
                return repo.save(existing);
            });
        }
    
        @Transactional
        public boolean delete(Long id) {
            if (!repo.existsById(id)) return false;
            repo.deleteById(id);
            return true;
        }
    }
    
  5. 5
    application-dev.yml — настройка JPA
    
    spring:
      jpa:
        hibernate:
          ddl-auto: validate   # НЕ создаёт таблицы сам — Flyway уже создал
        show-sql: true         # печатает SQL в консоль
        properties:
          hibernate.format_sql: true
          hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
        open-in-view: false    # убираем lazy-loading warning'и (см. вопрос 4)
    
  6. 6
    Запуск
    
    ./gradlew bootRun
    

    Проверяем:

    
    $ curl -X POST http://localhost:8080/api/tasks \
      -H "Content-Type: application/json" \
      -d '{"title":"Read JPA docs","description":"Understand @Entity","done":false}'
    
    # В консоли увидишь SQL:
    # Hibernate: insert into tasks (created_at, description, done, title, updated_at) values (?, ?, ?, ?, ?)
    # Hibernate: select t1_0.id, t1_0.created_at, ... from tasks t1_0 where t1_0.id=?
    

    ---

🎯

Зачем это на собесе

После урока ты должен уметь ответить на:

Меньше кода: findById сам конвертит ResultSet в Task.
Cache: JPA имеет L1-cache (по умолчанию в рамках транзакции) — повторный findById в той же транзакции не идёт в БД.
Lazy-loading: связанные сущности подгружаются по требованию (но это палка о двух концах — обсудим в Дополнении).
Repository pattern: тестировать легко — mock(TaskRepository.class).

5 вопросов на углубление

Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.

1
IDENTITY vs SEQUENCE — что выбрать для PostgreSQL?

Разбор внутри: генерация id, batch insert и практичный выбор для Postgres.

2
Что делает spring.jpa.hibernate.ddl-auto и почему validate безопаснее?

Разбор внутри: create, update, validate и почему схему лучше вести миграциями.

3
Зачем нужны @PrePersist и @PreUpdate?

Разбор внутри: автоматическое заполнение createdAt/updatedAt без ручного кода в сервисах.

4
Почему open-in-view мешает JPA-приложению?

Разбор внутри: ленивые связи, лишние SQL-запросы и честные границы транзакций.

5
Почему EntityManager требует транзакцию?

Разбор внутри: persist/update/delete должны выполняться внутри управляемой транзакции.

Готов идти дальше?

Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.

🚀 Перейти к Уроку 7

Сначала пройди все секции и выбери хотя бы 1 вопрос

⬅️ Назад к Уроку 5
🎉
Новый тир
Новый тир достигнут!