1. Querydsl 이란?

JPA를 이용해서 복잡한 쿼리나 동적 쿼리를 작성할 경우 소스코드가 지저분해지는 경우가 많습니다.
이때 Querydsl 라이브러리를 이용해면 쿼리문자가 아니라 자바코드로 쿼리를 작성할 수 있습니다.

 

2. Querydsl의 장점

1. 자바코드로 쿼리를 작성하기 때문에 가독성이 좋아집니다.
2. 문법 오류를 컴파일 시점에서 잡아줍니다.
3. 동적 쿼리를 쉽게 만들 수 있습니다.

 

3. Querydsl 설정하기

테스트 환경은 다음과 같습니다. (버전을 모두 맞춰 주어야합니다!)
spring boot 2.2.2 RELEASE
maven
java 11
h2

1) pom.xml

...

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>4.1.4</version>
        </dependency>

        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <version>4.1.4</version>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
 ...
    
   <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.mysema.maven</groupId>
                <artifactId>apt-maven-plugin</artifactId>
                <version>1.1.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>process</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>target/generated-sources/java</outputDirectory>
                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
...
querydsl-apt : Q클래스 생성 라이브러리입니다.
querydsl-jpa : querydsl 관련 라이브러리입니다.

apt-maven-plugin을 이용해서 target/generated-sources/java package에 엔티티 별로 Q클래스를 생성합니다.

 

2) src/main/resources/application.properties

spring.datasource.username=sa
spring.datasource.password=
spring.datasource.url=jdbc:h2:mem:mydb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.hikari.jdbc-url=jdbc:h2:mem:mydb
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true
h2 database 설정 및 hibernate 설정을 해줍니다.

 

3) Member.java

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

import javax.persistence.*;

@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String account;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String name;

}
Member 엔티티를 정의하고 mvn package를 하면 target/generated-sources/java package에 엔티티 별로 Q클래스가 생성됩니다. 
Q클래스는 querydsl을 이용해 엔티티의 컬럼에 접근하거나 다양한 쿼리구문을 작성할 때 사용합니다. 

 

4) MemberTest.java

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
@Commit
class MemberTest {

    @Autowired
    private EntityManager em;
    final String name = "member1";

    @BeforeEach
    public void setup(){
        Member member = Member.builder()
                .account("member@email.com")
                .name(name)
                .password("1234")
                .build();

        em.persist(member); // 미리 member를 생성한다.
    }

    @Test
    public void membersSearchTest() {
        JPAQueryFactory query = new JPAQueryFactory(em);
        QMember qMember = QMember.member;


        //querydsl을 이용해 java구문으로 query를 작성한다.
        Member member = query
                .select(qMember)
                .from(qMember)
                .where(qMember.name.eq(name))
                .fetchOne();

        assertEquals(member.getName(), name);
    }

}
Querydsl을 사용하려면 EntityManager객체를 생성자로 받은 JPAQueryFactory 객체가 필요합니다.
위 테스트를 진행하면 다음과 같은 쿼리가 DB로 전송됩니다.

---------------------------------------------------------
   select
        member0_.id as id1_0_,
        member0_.account as account2_0_,
        member0_.name as name3_0_,
        member0_.password as password4_0_ 
    from
        member member0_ 
    where
        member0_.name=?
---------------------------------------------------------

1. @MappedSuperclass란?

엔티티 별로 공통 필드가 존재하는 경우 불 필요한 중복 코드를 제거하기 위해 사용합니다.

 

2. 사용방법

1) BaseEntity.java

import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@MappedSuperclass
public abstract class BaseEntity {

    private LocalDateTime createDt;
    private LocalDateTime updateDt;

}
@MappedSuperclass 어노테이션을 클래스명 위에 선언하며, 공통 클래스를 별도로 생성하는 일이 없다면 추상 클래스로 선언하는 것이 좋습니다. 등록일, 수정일 같이 여러 테이블에서 공통으로 사용되는 필드를 테이블에 컬럼으로 등록할 때 사용합니다.

 

2) Product.java

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Product extends BaseEntity{
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int price;

}
BaseEntity 클래스를 상속 받으면 BaseEntity에 선언된 변수들이 컬럼으로 등록됩니다.

1. 상속 관계 매핑이란?

슈퍼타입 테이블, 서브타입 테이블을 자바에서 상속관계인 것 처럼 사용하는 방법입니다.

 

2. 상속 관계 매핑 전략의 종류

1) JOINED (조인전략)

슈퍼타입과 서브타입을 식별관계로 사용하는 방법입니다.

 

2) SINGLE_TABLE (단일 테이블 전략)

슈퍼타입과 서브타입을 하나의 테이블로 생성하는 전략입니다.


3) TABLE_PER_CLASS (타입 별 테이블 생성 전략) (비추천)

파일 타입 별로 테이블을 생성하는 전략입니다.

3. 상속 관계 매핑 적용

1) FileInfo.java

import javax.persistence.*;

@Entity
@Table(name = "file_info")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "data_type")
public abstract class FileInfo {
    @Id
    @GeneratedValue
    private Long id;

    private String url;

    @Column(name = "org_name")
    private String orgName;

    private String length;
    
    /*
    setter, getter 생략
    */
}
슈퍼 타입 클래스입니다. JOINED 전략을 사용할 경우 슈퍼 타입과 서브 타입은 식별관계가 되므로 서브 타입은 슈퍼타입 기본키를 외래키로 가짐과 동시에 기본키로 선언합니다.

@Inheritance 어노테이션을 이용해 JOINED, SINGLE_TABLE, TABLE_PER_CLASS 전략을 선택할 수 있으며, JOINED 전략을 보통 사용합니다. 하지만 검색속도 최적화를 위해 SINGLE_TABLE 전략을 사용하기도 합니다.
(TABLE_PER_CLASS 전략은 웬만하면 사용하지 않는 것을 추천합니다.)

@DiscriminatorColumn 어노테이션은 JOINED, SINGLE_TABLE 전략일 경우 사용하며, 서브타입의 구분값을 저장하는 컬럼입니다. name 속성을 이용해 구분 컬럼의 이름을 지정할 수 있습니다.

 

2) Image.java, Sound.java, Video.java

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue(value = "IMAGE")
public class Image extends FileInfo {

    private String type;

    private int width;
    private int height;
    
    /*
    setter,getter 생략
    */
}

@Entity
public class Sound extends FileInfo {

    private Long runtime;
    private String type;
    
    /*
    setter,getter 생략
    */    
}

@Entity
public class Video extends FileInfo {

    private Long runtime;

    private String type;
    
    /*
    setter,getter 생략
    */
}
서브 타입 클래스입니다. @DiscriminatorColumn 어노테이션을 사용했을 경우 서브타입 구분 값에 클래스 명이 저장됩니다.
@DiscriminatorValue 어노테이션을 사용했을 경우 서브타입 구분 값을 지정할 수 있습니다.

 

4. 테스트

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-persistence");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();
        try {
            Image image = new Image();

            image.setOrgName("google_icons.png");
            image.setUrl("https://ssl.gstatic.com/gb/images/p1_c9bc74a1.png");
            image.setType("png");

            //image 영속화
            em.persist(image);
            
            // 쓰기 지연에 쌓아둔 SQL을 DB에 전송한다.
            em.flush();
            // 영속성 캐시를 제거한다.
            em.clear();

            //image 조회
            Image findImage = em.find(Image.class, image.getId());

            System.out.println(findImage.getUrl());
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();

    }
}

 

@Inheritance(strategy = InheritanceType.JOINED) 전략으로 테스트했으며, Image를 저장하면 file_info와 image 테이블에 각각 저장됩니다. 

그리고 image를 조회할 경우 file_info와 image 테이블을 조인해서 select 합니다. 

 

# commit 시 작동되는 쿼리

# Image 저장 시, file_info와 image 테이블에 각각 저장한다.
        insert 
        into
            file_info
            (length, org_name, url, data_type, id) 
        values
            (?, ?, ?, 'IMAGE', ?)
		
        insert 
        
        into
            Image
            (height, type, width, id) 
        values
            (?, ?, ?, ?)
            
# image 조회 시, image 테이블과 file_info 테이블을 조인해서 select한다             
    select
        image0_.id as id2_0_0_,
        image0_1_.length as length3_0_0_,
        image0_1_.org_name as org_name4_0_0_,
        image0_1_.url as url5_0_0_,
        image0_.height as height1_1_0_,
        image0_.type as type2_1_0_,
        image0_.width as width3_1_0_ 
    from
        Image image0_ 
    inner join
        file_info image0_1_ 
            on image0_.id=image0_1_.id 
    where
        image0_.id=?            

도움이 되셨다면 공감 버튼 한번씩 눌러주세요!

Spring Boot에서 Hibernate 테스트를 하고 있던 도중 예기치못한 상황을 마주했습니다.

영속성이 끝났다고 생각되는 시점에서 프록시 객체를 조회하면 LazyInitializationException - No Session에러가 발생해야 하는데 Select Query가 실행되는 것이었습니다.

웹 검색을 통해 다음과 같은 사실을 알았습니다.

Open Session In View

Transaction이 종료된 후에도 Controller의 Session이 close되지 않았기 때문에, 영속 객체는 Persistence 상태를 유지할 수 있으며, Session이 열려있고 Persistence 상태이기 때문에 프록시 객체에 대한 Lazy Loading을 수행할 수 있게 됩니다.

출처: https://kingbbode.tistory.com/27 [개발노트 - kingbbode]

위 기능을 사용하지 않으려면 application.properties에 다음과 같은 옵션을 추가해주세요.

spring.jpa.open-in-view=false

 

1. Hibernate란

  • Boss에서 개발한 ORM(Object Relation Mapping)프레임워크 중 한개입니다.
  • ORM이란 객체와 DB 테이블의 매핑을 의미합니다.
  • ORM 매핑을 이용하면 객체로 DB 테이블 조작할 수 있습니다.

2. pom.xml

     ...
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
        </dependency>
      ... 
  • Spring Boot 버전 - 2.1.0.RELEASE
  • hibernate-core-5.3.7.Final

프로젝트 구조

 


3. application.yml

spring :
    datasource :
        url : jdbc:mysql://localhost:3306/db_name
        username : (DB User Name)
        password : (DB Password)
    jpa :
        properties :
            hibernate :
                format_sql:true
        show-sql : true        
        hibernate :
            ddl-auto : update    
            # ddl-auto - @Entity 테이블 정보를 실제 DB에 반영 할건지 설정(create/update/none..)

 


4. User.java

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(length = 20, nullable = false)
    private String name;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  • @Entity 어노테이션은 해당 클래스가 DB Table과 1:1 매핑이 될 수 있게 합니다.
  • @Id - Primery Key 지정
  • @GeneratedValue(strategy = GenerationType.IDENTITY) - 기본키 생성을 DB에게 위임합니다.
    Mysql에 경우, AUTO_INCREMENT를 사용합니다.
  • @Column(length = 20, nullable = false) - 테이블 컬럼을 선언합니다. (길이 20, null을 허용하지 않음)
  • 위 클래스 생성 후, 서버를 실행 시키면 실제 DB에 해당 테이블에 대한 정보가 반영됩니다.

서버 구동 시 아래와 같이 로그에 user테이블 생성 로그가 남았다면, 실제로 테이블이 생성되었는지 확인


5. UserDao.java 생성

import com.example.bamdule.model.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserDao extends JpaRepository<User, Integer> {

}

6. UserController.java 생성

import com.example.bamdule.dao.UserDao;
import com.example.bamdule.model.entity.User;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(value = "/user")
public class UserController {

    @Autowired
    private UserDao userDao;

    @GetMapping(value = "/save")
    @ResponseBody
    public List<User> saveUser(User user) {
        userDao.save(user);
        
        return userDao.findAll();
    }
}
  • user save 요청 추가
  • "/user/save" 요청 받는 유저를 DB에 저장하고, 유저 리스트를 반환합니다.
  • host/{contextPath}/user/save?name=park

브라우저에 위와 같은 응답이 왔으면 하이버네이트가 정상 동작 하는 것입니다.

+ Recent posts