Интеграционные тесты для выявления проблем спящего режима N + 1 - PullRequest
0 голосов
/ 29 января 2020

Попытка обнаружить проблему 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 не помогает решить Это! Я не понимаю, что я делаю не так. Буду рад любой помощи!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...