Содержание:
- Основные понятия
- Entity
- Статусы жизненного цикла Entity объекта
- Трансляция данных в БД | Ключевое слово flush
- Аннотации
- Основные шаги работы с Hibernate | Spring Data JPA
- Отношение One to Many
- Отношение Many to many
Все технологии и фреймворки в Java работают на спецификациях (стандартах, правилах). Они используются для того, чтобы не было хаоса, разных версий и тракотовок.
JPA - Java Persistence API - спецификация, документ, в котором описаны правила и API для реализации принципов ORM для Java (аннотации, настройки, подход).
ORM - Object-Relational Mapping - концепция, позволяющая работать с БД как с объектами.
Представляет собой еще один слой между взаимодействием кода и БД, напрямую код к БД не обращается, хотя это возможно.
Hibernate одна из самых популярных открытых реализаций последней версии спецификации. JPA только описывает правила и API, а Hibernate реализует эти описания, впрочем у Hibernate (как и у многих других реализаций JPA) есть дополнительные возможности, не описанные в JPA (и не переносимые на другие реализации JPA).
Entity - сущность - объект бизнес-логики. Основная программаная сущность - это entity класс, который так же может использовать вспомогательные классы.
@Entity
- Эта аннотация указывает Hibernate, что данный класс является сущностью (entity bean). Такой класс должен иметь
конструктор по-умолчанию (пустой конструктор)
Entity класс может
- наследоваться как от других Entity классов, так и от не Entity классов;
- иметь не Entity наследника;
- может быть абстрактным классом;
EntityManager - интерфейс, описывающий API для всех основных операций над Entity, получние данных и других сущностей JPA. Главный API для работы с JPA.
Основные оперции:
- операции над Entity:
- persist - добавление Entity под управление JPA;
- merge - обновление;
- remove - удаление;
- refresh - обновление данных;
- detach - удаление из отслеживания JPA;
- lock - блокирование Entity от изменения других thread;
- получение данных;
- работа с EntityGraph;
- общие операции;
- close;
- isOpen;
- getProperties;
- setProperty;
- clear;
- new - объект создан, но при этом еще не имеет сгенерированного первичного ключа и пока не сохранен в базе данных.
- managed - объект создан, управляется JPA, имеет сгенерированные ключи.
- detached - объект был создан, но не управляется (или больше не управляется) JPA.
- removed - объект создал, управляется JPA,но будет удален после commit'a транзакции.
При переводе JPA сущностей из одного состояния в другое, то есть при вызове методов сохранения и
удаления (persist()
, merge()
, remove()
), немедленного выполнения SQL-команд не происходит.
SQL-команды накапливаются, а выполнение их откладывается до:
- Подтверждения транзакции
commit()
. - Выполнения JPQL и HQL запросов.
- Выполнения native SQL запросов.
- Вызова метода
flush()
@Table
- С помощью этой аннотации мы говорим Hibernate, с какой именно таблицей необходимо связать (map) данный класс.
Аннотация @Table имеет различные аттрибуты, с помощью которых мы можем указать имя таблицы, каталог, БД и уникальность столбцов в таблец БД.
@Id
- С помощью аннотации @Id мы указываем первичный ключ (Primary Key) данного класса. Hibernate содает индекс по этому
полю и делает этот индекс уникальным (Unique)
@GeneratedValue
- Эта аннотация используется вместе с аннотацией @Id и определяет такие паметры, как strategy и generator.
@Column
- Аннотация @Column определяет к какому столбцу в таблице БД относится конкретное поле класса (аттрибут класса).
Наиболее часто используемые аттрибуты аннотации @Column такие:
- name - Указывает имя столбца в таблице
- unique - Определяет, должно ли быть данноезначение уникальным
- nullable - Определяет, может ли данное поле быть NULL, или нет.
- length - Указывает, какой размер столбца (например колчиство символов, при использовании String).
- Конфигурация приложения. Hibernate позволяет конфигурировать взаимодействие с БД через файл конфигурации или при
помощи применения Java-аннотаций. Spring Boot предоставляет возможность конфигурации в файле
application.properties
Файл application.properties:
# Spring properties
spring.datasource.url=URL
spring.datasource.username=USERNAME
spring.datasource.password=PASSWORD
# Hibernate properties
spring.jpa.database-platform = org.hibernate.dialect.PostgreSQL94Dialect
spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto = update
spring.jpa.hibernate.naming.implicit-strategy = org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
spring.jpa.properties.hibernate.format_sql=true
- Создание класса-сущности. Пакет
model
, POJO-класс, который получит отображение в виде сущности в БД.
// POJO-образующие
@Data
// Класс является сущностью
@Entity
// В БД отображается в таблицу с именем students
@Table(name = "students")
public class Student {
// Поле первичного ключа
@Id
// Автоинкремент
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Столбец в таблице, может ли быть null
@Column(name = "first_name", nullable = false)
@NotEmpty(message = "wrong first name")
@Size(min = 2, max = 50, message = "Parameter's length must be between 2 and 50 symbols")
private String firstName;
@Column(name = "last_name", nullable = false)
@NotEmpty(message = "wrong last name")
private String lastName;
@Column(name = "email", nullable = false)
@NotEmpty(message = "Wrong email")
@Email(message = "Wrong email")
private String email;
}
- Создание репозитория. Пакет
repository
, интерфейс наследуется от JpaRepository<ТипОбъекта, ТипId> - класса, реализующего основной функционал и API для взаимодействия с JPA.
public interface StudentRepository extends JpaRepository<Student, Long> {}
Аннотация @Repository указывать не нужно, так как вышестоящие по иерархии классы уже содержат ее.
- Обработка исключений. Пакет
excepitions
, обработка исключения, связанного с отсутствием требуемого ресурса.
// Возвращаемый статус-код
@ResponseStatus(value = HttpStatus.NOT_FOUND)
// Набор геттеров для класса
@Getter
public class ResourceNotFoundException extends RuntimeException {
private final String resourceName;
private final String fieldName;
private final Object fieldValue;
public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
this.resourceName = resourceName;
this.fieldName = fieldName;
this.fieldValue = fieldValue;
}
}
- Сервис-слой. Реализация бизнесс-логики при помощи средств JPA, пакет
service
.
Интерфейс
public interface StudentService {
Student saveStudent(Student student);
List<Student> getAllStudents();
Student getStudentById(Long id);
Student updateStudent(Long id, Student updatedStudent);
void deleteStudent(Long id);
}
Реализация
@Service
public class StudentServiceImpl implements StudentService {
private final StudentRepository studentRepository;
@Autowired
public StudentServiceImpl(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
@Override
public Student saveStudent(Student student) {
return studentRepository.save(student);
}
@Override
public List<Student> getAllStudents() {
return studentRepository.findAll();
}
@Override
public Student getStudentById(Long id) {
return studentRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Student", "Id", id));
}
@Override
public Student updateStudent(Long id, Student updatedStudent) {
Student existedStudent = studentRepository.findById(id).orElseThrow(
() -> new ResourceNotFoundException("Student", "Id", id));
existedStudent.setFirstName(updatedStudent.getFirstName());
existedStudent.setLastName(updatedStudent.getLastName());
existedStudent.setEmail(updatedStudent.getEmail());
studentRepository.save(existedStudent);
return existedStudent;
}
@Override
public void deleteStudent(Long id) {
studentRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Student", "Id", id));
studentRepository.deleteById(id);
}
}
- Контроллер. Пакет
controller
/
@RestController
@RequestMapping("/api/students")
public class StudentController {
private final StudentService studentService;
public StudentController(StudentService studentService) {
super();
this.studentService = studentService;
}
@PostMapping("/add")
public ResponseEntity<Student> saveStudent(@RequestBody Student student) {
return new ResponseEntity<Student>(studentService.saveStudent(student), HttpStatus.CREATED);
}
@GetMapping
public List<Student> getAllStudents() {
return studentService.getAllStudents();
}
@GetMapping("{id}")
public ResponseEntity<Student> getStudentById(@PathVariable("id") Long id) {
return new ResponseEntity<Student>(studentService.getStudentById(id), HttpStatus.OK);
}
@PutMapping("{id}/update")
public ResponseEntity<Student> updateStudent(@PathVariable("id") Long id, @RequestBody Student student) {
return new ResponseEntity<Student>(studentService.updateStudent(id, student), HttpStatus.OK);
}
@DeleteMapping
public ResponseEntity<String> deleteStudent(Long id) {
studentService.deleteStudent(id);
return new ResponseEntity<String>("Student deleted successfully!", HttpStatus.OK);
}
}
- Используется родительской сущностью для отображения коллекции дочерних сущностей;
- Виды:
- двунаправленная (bidirectional) - аннотация
@OneToMany
только на родительской стороне в то время как на дочерней@ManyToOne
- однонаправленная - отображение только на родительской стороне.
- двунаправленная (bidirectional) - аннотация
Дочерняя сущность:
@ManyToOne
private Post post;
Родительская сущность
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostComment> comments = new ArrayList<>();
public void addComment(PostComment comment) {
comments.add(comment);
comment.setPost(this);
}
public void removeComment(PostComment comment) {
comments.remove(comment);
comment.setPost(null);
}
Связывание
// Добавление
Post post = new Post("First post");
post.addCommet(new PostComment("My first review"));
post.addCommet(new PostComment("My second review"));
entityManager.persist(post);
// Удаление
Post post = entityManager.find(Post.class, 1L);
post.removeComment(post.getComments().get(0));
Связь между двумя родительскими сущностями осуществляется через дочернюю. Родители предствляют собой независимые сущности, не зависящие от других сущностей.
Виды
- однонаправленная - только одна сторона отображает отношение;
- двунаправленная - обе стороны отображают отношение.
The Post Entity
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(name = "post_tag", // промежуточная таблица
joinColumns = @JoinColumn(name = "post_id"), // поле для связи с дочерней сущностью
inverseJoinColumns = @JoinColumn(name = "tag_id") // поле для связи со вторым родителем
)
private Set<Tag> tags = new HashSet<>();
The Post Entity
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(name = "post_tag", // промежуточная таблица
joinColumns = @JoinColumn(name = "post_id"), // поле для связи с дочерней сущностью
inverseJoinColumns = @JoinColumn(name = "tag_id") // поле для связи со вторым родителем
)
private Set<Tag> tags = new HashSet<>();
// Методы для взаимосвязи
public void addTag(Tag tag) {
tags.add(tag); //добавили элемент в текущий объект
tag.getPosts().add(this); // Добавили текущий объект в связанный
}
public void removeTag(Tag tag) {
tags.remove(tag);
tag.getPosts().remove(this);
}
The Tag Entity
@ManyToMany(mappedBy = "tags") // Поле, с которым связываем
private Set<Post> posts = new HashSet<>();
Получение данных из связанных таблиц через вспомогательные сущности.
Вспомогательный класс PostTagId использует аннотацию @Embeddable
, чтобы объявить, что класс может быть встроен в другие объекты.
@Embeddable
public class PostTagId implements Serializable {
@Column(name = "post_id")
private Long postId;
@Column(name = "tag_id")
private Long tagId;
public PostTagId(){}
public PostTagId(Long postId, Long tagId) {
this.postId = postId;
this.tagId = tagId;
}
// Getter, Setter, HashCode, Equals etc
}
Класс PostTag связывает сущности Post и Tag.
Аннотация @EmbeddedId
указывает на id встраиваемого объекта, @MapsId
- поле встраиваемого объекта
@Entity
public class PostTag {
@EmbeddedId
private PostTagId id;
@ManyToOne
@MapsId("postId")
private Post post;
@ManyToOne
@MapsId("tagId")
private Tag tag;
}
При этом сущности Post и Tag больше не будут использовать аннотацию @ManyToMany
.
Вместо этого отношение обрабатывается как двунаправленная @OneToMany
Класс Post
@OneToMany(mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PostTag> tags = new ArrayList<>();
public void removeTag(Tag tag) {
for (Iterator<PostTag> interator = tags.iterator(); iterator.hasNext(); ) {
PostTag postTag = interator.next();
if (postTag.getPost().equals(this) && postTag.getTag().equals(tag)) {
iterator.remove();
postTag.getTag().getPosts().remove(postTag);
postTag.setPost(null);
postTag.setTag(null);
break;
}
}
}
Класс Tag
@OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostTag> posts = new ArrayList<>();