Попытка обнаружить проблему N + 1. Я использовал библиотеку Влада Михалча https://vladmihalcea.com/how-to-detect-the-n-plus-one-query-problem-during-testing/, но почему-то не учитывает количество запросов. Количество запросов всегда = 0 и тесты не пройдены. Я подключил перехватчик - и с его помощью можно посчитать количество вызовов.
Что я уже сделал:
1) Модель SongCompilation
package spring.app.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
@Entity
@Table(name = "song_compilation")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) //без этой аннотации LAZY не работало (по-моему не отображались песни)
public class SongCompilation {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "genre_id")
private Genre genre;
@JsonIgnore
@ManyToMany(targetEntity = Song.class)
@JoinTable(name = "song_compilation_on_song",
joinColumns = {@JoinColumn(name = "song_compilation_id")},
inverseJoinColumns = {@JoinColumn(name = "song_id")})
private Set<Song> song = new HashSet<>();
public SongCompilation() {
}
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 Genre getGenre() {
return genre;
}
public void setGenre(Genre genre) {
this.genre = genre;
}
public Set<Song> getSong() {
return song;
}
public void setSong(Set<Song> song) {
this.song = song;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SongCompilation that = (SongCompilation) o;
return Objects.equals(id, that.id) &&
Objects.equals(name, that.name) &&
Objects.equals(genre, that.genre);
}
@Override
public int hashCode() {
int result = id != null ? id.hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (genre != null ? genre.hashCode() : 0);
return result;
}
}
2) Модель Genre
package spring.app.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import javax.persistence.*;
import java.util.Set;
@Entity
@Table(name = "genre")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) //без этой аннотации LAZY не работало (по-моему не отображались песни)
public class Genre extends Bannable{
@Id
@GeneratedValue
private Long id;
private String name;
@JsonIgnore
@OneToMany(mappedBy = "genre")
private Set<SongCompilation> songCompilation;
/**
* Вспомогательное поле, кокоторое используеться фронтом для корректного отображения данных.
*/
@Transient
private Boolean banned;
public Genre(){}
public Genre(String name) {
this.name = name;
}
public Set<SongCompilation> getSongCompilation() {
return songCompilation;
}
public void setSongCompilation(Set<SongCompilation> songCompilation) {
this.songCompilation = songCompilation;
}
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Boolean isBanned() {
return banned;
}
@Override
public void setBanned(boolean banned) {
this.banned = banned;
}
@Override
public boolean isBannedBy(Company company) {
return company.getBannedGenres().contains(this);
}
@Override
public int hashCode() {
int result = id != null ? id.hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Genre genre = (Genre) o;
return id.equals(genre.id) &&
name.equals(genre.name);
}
}
3) GenreServiceImpl
package spring.app.service.impl;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import spring.app.dao.abstraction.GenreDao;
import spring.app.model.Genre;
import spring.app.service.abstraction.GenreService;
import spring.app.util.CrudInterceptor;
import java.util.List;
@Service
@Transactional
public class GenreServiceImpl implements GenreService {
private final GenreDao genreDao;
CrudInterceptor crudInterceptor = new CrudInterceptor();
@Autowired
public GenreServiceImpl(GenreDao genreDao) {
this.genreDao = genreDao;
}
@Override
public void addGenre(Genre genre) {
genreDao.save(genre);
}
@Override
public Genre getByName(String name) {
return genreDao.getByName(name);
}
@Override
public List<Genre> getAllGenre() {
return genreDao.getAll();
}
@Override
public List<Genre> getAllGenreDaoJoin() {
return genreDao.getAllDaoJoin();
}
@Override
@Fetch(FetchMode.JOIN)
public List<Genre> getAllGenreFetchModeJoin() {
return genreDao.getAll();
}
@Override
@BatchSize(size = 3)
public List<Genre> getAllGenreBatch() {
return genreDao.getAll();
}
@Override
@Fetch(FetchMode.SUBSELECT)
public List<Genre> getAllGenreFetchModeSubselect() {
return genreDao.getAll();
}
@Override
@Fetch(FetchMode.SUBSELECT)
@BatchSize(size = 3)
public List<Genre> getAllGenreFetchModeSubselectBatch() {
return genreDao.getAll();
}
@Override
public void updateGenre(Genre genre) {
genreDao.update(genre);
}
@Override
public void deleteGenreById(Long id) {
genreDao.deleteById(id);
}
@Override
public Genre getById(Long id) {
return genreDao.getById(id);
}
@Override
@Fetch(FetchMode.JOIN)
public Genre getByIdFetchModeJoin(Long id) {
return genreDao.getById(id);
}
}
4) GenreDaoImpl
package spring.app.dao.impl;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import spring.app.dao.abstraction.GenreDao;
import spring.app.model.*;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import java.util.List;
@Repository
@Transactional(readOnly = true)
public class GenreDaoImpl extends AbstractDao<Long, Genre> implements GenreDao {
GenreDaoImpl() {
super(Genre.class);
}
@Override
public Genre getByName(String name) {
TypedQuery<Genre> query = entityManager.createQuery("SELECT u FROM Genre u WHERE u.name = :name", Genre.class);
query.setParameter("name", name);
Genre genre;
try {
genre = query.getSingleResult();
} catch (NoResultException e) {
return null;
}
return genre;
}
@Override
public void deleteById(Long id) {
TypedQuery<SongCompilation> query = (TypedQuery<SongCompilation>) entityManager.createQuery("UPDATE SongCompilation SET genre_id = null WHERE genre_id = :id");
query.setParameter("id", id);
query.executeUpdate();
TypedQuery<Company> queryCompany = (TypedQuery<Company>) entityManager.createNativeQuery("DELETE FROM company_on_banned_genre WHERE genre_id = :id");
queryCompany.setParameter("id", id);
queryCompany.executeUpdate();
TypedQuery<OrgType> queryOrgType = (TypedQuery<OrgType>) entityManager.createNativeQuery("DELETE FROM org_type_on_related_genre WHERE genre_id = :id");
queryOrgType.setParameter("id", id);
queryOrgType.executeUpdate();
TypedQuery<Author> queryAuthor = (TypedQuery<Author>) entityManager.createNativeQuery("DELETE FROM author_on_genre WHERE genre_id = :id");
queryAuthor.setParameter("id", id);
queryAuthor.executeUpdate();
TypedQuery<Song> querySong = (TypedQuery<Song>) entityManager.createQuery("UPDATE Song SET genre_id = null WHERE genre_id = :id");
querySong.setParameter("id", id);
querySong.executeUpdate();
super.deleteById(id);
}
public List<Genre> getAllDaoJoin() {
TypedQuery<Genre> query = entityManager.createQuery("select g from Genre g " +
"join fetch g.songCompilation", Genre.class);
List<Genre> genreList;
try {
genreList = query.getResultList();
} catch (NoResultException e) {
return null;
}
return genreList;
}
}
5) Интеграционный тест
package spring.app.service.impl;
import com.vladmihalcea.sql.SQLStatementCountValidator;
import com.vladmihalcea.sql.exception.SQLSelectCountMismatchException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import spring.app.service.abstraction.GenreService;
import spring.app.util.CrudInterceptor;
import static com.vladmihalcea.sql.SQLStatementCountValidator.assertSelectCount;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.junit.Assert.assertEquals;
@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles(value = "application-test.yml") //security disable
public class GenreServiceImplTest {
@Autowired
private GenreService genreService;
@Test
public void contexLoads() throws Exception {
assertThat(genreService).isNotNull();
}
@Test
public void getAllGenreTest() throws Exception {
CrudInterceptor.reset();
genreService.getAllGenre();
assertEquals(1,CrudInterceptor.getCount());
}
@Test
public void getAllGenreDaoJoinTest() throws Exception {
CrudInterceptor.reset();
genreService.getAllGenreDaoJoin();
assertEquals(1,CrudInterceptor.getCount());
}
@Test
public void getAllGenreJoinTest() throws Exception {
CrudInterceptor.reset();
genreService.getAllGenreFetchModeJoin();
assertEquals(1,CrudInterceptor.getCount());
}
@Test
public void getAllGenreSubselectTest() throws Exception {
CrudInterceptor.reset();
genreService.getAllGenreFetchModeSubselect();
assertEquals(2, CrudInterceptor.getCount());
}
@Test
public void getAllGenreSubselectBatchTest() throws Exception {
CrudInterceptor.reset();
genreService.getAllGenreFetchModeSubselectBatch();
assertEquals(3, CrudInterceptor.getCount());
}
@Test
public void getAllGenreBatchTest() throws Exception {
CrudInterceptor.reset();
genreService.getAllGenreBatch();
assertEquals(3, CrudInterceptor.getCount());
}
@Test
public void getAllGenreVladTest() throws Exception {
try {
SQLStatementCountValidator.reset();
genreService.getAllGenre();
assertSelectCount(1);
} catch (SQLSelectCountMismatchException e) {
assertEquals(1, e.getRecorded());
}
}
@Test
public void getGenreByIdJoinTest() throws Exception {
CrudInterceptor.reset();
genreService.getByIdFetchModeJoin(1L);
assertEquals(1,CrudInterceptor.getCount());
}
@Test
public void getGenreByIdTest() throws Exception {
CrudInterceptor.reset();
genreService.getById(1L);
assertEquals(1, CrudInterceptor.getCount());
}
}
6) Перехватчик
package spring.app.util;
import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;
import java.io.Serializable;
public class CrudInterceptor extends EmptyInterceptor {
private static int count;
public static int getCount() {
return count;
}
public static void setCount(int count) {
CrudInterceptor.count = count;
}
public static void reset() {
count = 0;
}
@Override
public boolean onLoad(
Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
count++;
System.out.println("Load method called " + count + " times");
return super.onLoad(entity, id, state, propertyNames, types);
}
@Override
public boolean onSave(
Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
count++;
System.out.println("Save method called " + count + " times");
return super.onLoad(entity, id, state, propertyNames, types);
}
@Override
public void onDelete(
Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
System.out.println("Ondelete Method Called: " + entity + " SerializableId: " + id + " state: " + state);
}
}
7) Регистрация перехватчика в application.properties
spring.jpa.properties.hibernate.ejb.interceptor=spring.app.util.CrudInterceptor
С перехватчиками - почти во всех тестах возникает проблема N + 1, и JOIN FETCH не помогает решить Это! Я не понимаю, что я делаю не так. Буду рад любой помощи!