해당 글은 김영한 님의 자바 ORM 표준 JPA 프로그래밍을 참고하여 작성한 글입니다.

 

✅ JPQL이란?

JPQL(Java Persistence Query Language): 엔티티 객체를 조회하기 위해 설계된 객체 지향 쿼리 언어

 

▶ 특징

  • JPQL은 데이터베이스의 테이블이 아닌 엔티티 객체를 대상으로 하는 쿼리 언어이다.
  • SQL을 추상화했기 때문에 특정 데이터베이스 시스템에 의존하지 않는다.
  • SQL과 유사한 문법을 가지며, JPQL은 내부적으로 SQL로 변환되어 실행된다.

 

▶ 기본 문법

select_문 ::=        
    select_절        
    from_절        
    [where_절]        
    [groupby_절]        
    [having_절]        
    [orderby_절]

update_문 ::= update_절 [where_절]

delete_문 ::= delete_절 [where_절]

 

▶ SQL과 다른 특징

select m
from Member as m
where m.age > 18
  1. 엔티티와 속성은 대소문자를 구분한다.
    • 예: Member(엔티티), age(속성)은 대소문자를 구분해 줘야 한다.
  2. JPQL 키워드는 대소문자를 구분하지 않아도 된다.
    • 예: SELECT, FROM, WHERE 같은 JPQL 키워드는 대소문자를 구분하지 않아도 된다.
  3. 엔티티 이름을 사용해야 한다.
    • 예: Member는 클래스가 아니라 엔티티 이름이다.
    • 엔티티 이름은 @Entity(name="Member")로 지정할 수 있으며, 별도로 지정하지 않으면 클래스 이름이 기본값으로 사용된다.
  4. 별칭은 필수이다.
    • 예: Member as m에서 Member에 m이라는 별칭을 부여했다.
    • as 키워드는 생략할 수 있다.

 

▶ 집합과 정렬

관계형 데이터베이스의 집합과 정렬 등의 함수를 제공한다.

select
	COUNT(m),   //회원 수
	SUM(m.age), //나이 합
	AVG(m.age), //평균 나이
	MAX(m.age), //최대 나이
	MIN(m.age)  //최소 나이
from Member m
  • GROUP BY, HAVING, ORDER BY 등 모두 사용이 가능하다.

 

▶ 문제점

  • JPQL은 기본 문자열로 작성되기 때문에 컴파일 시 에러를 잡아내지 않는다. 따라서 문제가 있음에도 불구하고 정상적으로 작동하여 배포 시 문제가 발생할 수 있다.
  • 동적으로 쿼리를 작성할 때 비효율적이다. (예: 특정 조건에 따라 쿼리를 다르게 작성해야 하는 경우 등)

 

 

 

✅ TypeQuery, Query

▶ TypeQuery

반환 타입이 명확할 때 사용한다.

🔽 예시 코드

//TypedQuery
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class);
  • 반환 타입이 명확할 경우, 두 번째 파라미터에 타입 정보를 작성할 수 있다.
    • 기본적으로 엔티티 타입을 기입한다.
    • query2와 같이 string인 username의 정보는 String class 타입 정보를 기입할 수 있다.

 

▶ Query

반환 타입이 명확하지 않을 때 사용한다.

🔽 예시 코드

//Query
Query query = em.createQuery("select m.username, m.age from Member m");
  • String 타입인 username과 int 타입인 age를 함께 조회할 때에는 반환 타입이 명확하지 않다.

 

 

 

✅ 결과 조회 API

▶ getResultList()

결과가 하나 이상일 경우 사용하며, 리스트로 반환한다.

🔽 예시 코드

TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
List<Member> resultList = query.getResultList(); //getResultList()

🔽 특징

  • 결과가 없을 경우 빈 리스트를 반환한다.

 

▶ getSingleResult()

결과가 정확히 하나일 경우 사용하며, 단일 객체를 반환한다.

🔽 예시 코드

TypedQuery<Member> query = em.createQuery("select m from Member m where m.id = 1L", Member.class);
Member result = query.getSingleResult(); //getSingleResult()

🔽 특징

  • 결과가 없을 경우 NoResultException 예외가 발생한다.
    • 결과가 없을 경우 예외가 발생하는 것은 일반적이지 않는데, Spring Data JPA를 사용할 경우 null이나 Optional을 반환하는 방식으로 처리한다.
  • 결과가 두 개 이상일 경우 NonUniqueResultException 예외가 발생한다.

 

 

 

✅ 파라미터 바인딩

▶ 이름 기준

이름 기준 파라미터 바인딩은 =: 을 사용해서 설정한다.

select m from Member m where m username =:username // =: 이름 기준 파라미터 바인딩

🔽 예시 코드

TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = :username", Member.class);
query.setParameter("username", "member1"); //이름 기준 파라미터 바인딩
Member result = query.getSingleResult();

🔽 메서드 체인 사용 예시 코드

Member result = em.createQuery("select m from Member m where m.username = :username", Member.class)
                .setParameter("username", "member1") //이름 기준 파라미터 바인딩
                .getSingleResult();

 

▶ 위치 기준

위치 기준 파라미터 바인딩은 =? 을 사용해서 설정한다.

select m from Member m where m username = ?1 // =? 위치 기준 파라미터 바인딩

🔽 예시 코드

TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = ?1", Member.class);
query.setParameter(1, "member1"); //위치 기준 파라미터 바인딩
Member result = query.getSingleResult();

🔽 메서드 체인 사용 예시 코드

Member result = em.createQuery("select m from Member m where m.username = ?1", Member.class)
                .setParameter(1, "member1") //위치 기준 파라미터 바인딩
                .getSingleResult();
⚠️ 위치 기준 파라미터 바인딩은 사용하지 않는 것이 좋다. 데이터베이스에는 값이 계속해서 추가되기 때문에, 위치가 변경될 가능성이 높기 때문이다.

 

 

 

✅ 프로젝션

프로젝션: JPQL의 SELECT 절에 조회할 대상을 지정하는 것

  • SELECT 절의 조회 대상의 타입에 따라 프로젝션 종류가 달라진다.
    • 엔티티 프로젝션
    • 임베디드 타입 프로젝션
    • 스칼라 타입 프로젝션

 

▶ 엔티티 프로젝션

엔티티 프로젝션은 SELECT 절의 조회 대상이 엔티티 객체인 경우를 말한다.

select m from Member m
select m.team from Member m

🔽 예시 코드

List<Member> resultList = em.createQuery("select m from Member m", Member.class).getResultList();

🔽 특징

Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);

em.flush();
em.clear();

List<Member> resultList = em.createQuery("select m from Member m", Member.class).getResultList();

Member findMember = resultList.get(0);
findMember.setAge(20);
  • 엔티티 프로젝션은 영속성 컨텍스트에서 관리된다.
    • 위와 같이 영속성 컨텍스트가 완전히 비워진 상태에서 엔티티 프로젝션의 조회를 할 경우, 쿼리 결과를 통해 엔티티 속성을 변경할 수 있다.

 

 

▶ 임베디드 타입 프로젝션

임베디드 타입 프로젝션은 SELECT 절의 조회 대상이 엔티티의 값 타입 속성인 경우를 말한다.

select m.address from Member m

🔽 예시 코드

List<Address> resultList = em.createQuery("select m.address from Member m", Address.class).getResultList();

🔽 특징

//JPQL에서 불가능한 쿼리 (
select a from Address a
  • 임베디드 값 타입은 특정 엔티티에 속해 있기 때문에 조회의 시작점이 될 수 없다.
    • 위와 같은 쿼리는 불가능하다.

 

 

▶ 스칼라 타입 프로젝션

스칼라 타입 프로젝션은 SELECT 절의 조회 대상이 데이터 타입과 상관없이, 여러 데이터를 조회하는 경우를 말한다.

select m.username, m.age from Member m

🔽 예시 코드

List resultList = em.createQuery("select m.username, m.age from Member m").getResultList();
  • 설명: username은 String 타입, age는 int 타입인데, 이 경우 결과 데이터를 조회하기 위한 여러 방법들이 존재한다.

🔽 여러 데이터를 조회하는 방법

1. Query 타입으로 조회

Query query = em.createQuery("select m.username, m.age from Member m");

2. 타입 캐스팅을 이용한 Object[] 타입으로 조회

List resultList = em.createQuery("select m.username, m.age from Member m").getResultList();

Object o = resultList.get(0);
Object[] result = (Object[]) o; //타입 캐스팅

System.out.println("result[0] = " + result[0]);
System.out.println("result[0] = " + result[1]);
  • 설명: 위 조회 결과 List는 Object 타입으로 저장되는데, 따라서 각각의 username과 age를 조회하기 위해서는 배열로 캐스팅해야 한다.
List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m").getResultList();

Object[] result = resultList.get(0);

System.out.println("result[0] = " + result[0]);
System.out.println("result[0] = " + result[1]);
  • 설명: 위와 같이 제너릭을 활용하여 타입 캐스팅도 가능하다.

3. new 명령어를 통한 DTO 조회

@Getter
@Setter
@AllArgsConstructor //생성자 필수
public class MemberDTO {

    private String username;
    private int age;
}
  • 조회 용도의 DTO를 생성한다.
List<MemberDTO> resultList = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
                    .getResultList();

MemberDTO memberDTO = resultList.get(0);

System.out.println("memberDTO = " + memberDTO.getUsername());
System.out.println("memberDTO = " + memberDTO.getAge());
  • SELECT 절에는 new 명령어와 함께 패키지 명을 포함한 전체 클래스 명을 작성한다.
  • 순서와 타입이 일치하는 생성자가 필수이다.
  • 패키지명을 작성해야 하는 불편함은 QueryDSL을 사용하여 극복할 수 있다.
3번 방법을 사용하는 것이 권장된다.

 

 

 

✅ 페이징 API

JPA는 페이징을 다음 두 가지 API로 추상화하여 제공한다.

  • setFirstResult(int startPosition): 조회 시작 위치 (0부터 시작)
  • setMaxResults(int maxResult): 조회할 데이터 수

🔽 예시 코드

List<Member> resultList = em.createQuery("select m from Member m order by m.age desc", Member.class)
        .setFirstResult(0) //조회 시작
        .setMaxResults(10) //조회할 데이터 수
        .getResultList();
  • 설명: Member의 나이를 기준으로 내림차순(desc)으로 0부터 10개의 데이터를 조회한다.

 

 

 

✅ 조인

JPQL은 SQL과 마찬가지로 조인(JOIN)을 지원하는데, SQL 문법과는 약간 차이가 있다.

 

▶ 내부 조인(Inner Join)

내부 조인은 INNER JOIN을 사용한다. 

select m from Member m [inner] join m.team t
  • INNER는 생략이 가능하다.

🔽 예시 코드

String query = "select m from Member m inner join m.team t";
List<Member> resultList = em.createQuery(query, Member.class).getResultList();
  • JPQL 조인은 연관 관계를 사용한다.
    • Member m join m.team t: Member가 가지고 있는 연관 관계 필드로 Team과 조인

 

 

▶ 외부 조인(Outer Join)

외부 조인은 LEFT OUTER JOIN을 사용한다.

select m from Member m left [outer] join m.team t
  • OUTER은 생략이 가능하다.

🔽 예시 코드

String query = "select m from Member m left outer join m.team t";
List<Member> resultList = em.createQuery(query, Member.class).getResultList();

 

 

▶ 세타 조인(Theta Join)

JPQL에서는 기본적으로 연관관계가 있는 엔티티들 간의 조인이 이루어진다. 그러나 세타 조인은 연관관계가 전혀 없는 엔티티들 간의 조인이 가능하기 때문에, 서로 연관관계가 없을 때 사용하는 방법 중 하나이다.

 

🔽 특징

  • 세타 조인은 WHERE 절을 사용하여 작성할 수 있다.
  • 세타 조인은 내부 조인만 지원한다.
  • 전혀 관계가 없는 엔티티들 간의 조인이 가능하다.

🔽 예시 코드

String query = "select count(m) from Member m, Team t where m.username = t.name";
List<Member> resultList = em.createQuery(query, Member.class).getResultList();

 

 

▶ 조인 - ON절

JPA 2.1부터 조인할 때 ON 절을 지원하며, ON 절은 조인 대상을 필터링한 후 조인할 수 있게 해준다.

  • 내부 조인에서 ON 절은 WHERE 절을 사용할 때와 결과가 같다.
  • 보통 ON 절은 외부 조인에서만 사용한다.
  • Hibernate 5.1부터 연관관계가 없는 엔티티들 간의 외부 조인이 가능하다.

🔽 조인 대상 필터링

  • JPQL
select m, t from Member m left join m.team t on t.name = 'A'
  • SQL
SELECT m.*, t.*
FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'

🔽 연관관계없는 엔티티 외부 조인

  • JPQL
select m, t from Member m left join Team t on m.username = t.name
  • SQL
SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name

 

 

 

✅ 서브 쿼리

JPQL도 SQL처럼 서브 쿼리를 지원한다.

 

▶ 서브 쿼리 지원 함수

🔽 [NOT] EXISTS

  • EXISTS: 서브 쿼리에 결과가 존재하면 참이다.
  • NOT EXISTS: 서브 쿼리에 결과가 없을 때 참이다.
//팀A 소속인 회원
select m from Member m where exists (select t from m.team t where t.name = 'teamA')

🔽 ALL | ANY | SOME

  • ALL: 서브 쿼리의 결과가 모두 만족하면 참이다.
  • ANY, SOME: 서브 쿼리의 결과 중 하나라도 만족하면 참이다.
  • 비교 연산자(=, <>, <, <=, >, >=)와 함께 사용된다.
//전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o where o.orderAmount > ALL (select p.stockAmount from Product p)
//어떤 팀이든 팀에 소속된 회원
select m from Member m where m.team = ANY (select t from Team t)

🔽 [NOT] IN

  • IN: 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참이다.
  • NOT IN: 서브 쿼리의 결과 중 어느 것도 같은 것이 없을 때 참이다.
  • IN은 서브 쿼리가 아닌 곳에서도 사용한다.
//20세 이상을 보유한 팀
select t from Team t where t IN (select t2 From Team t2 JOIN t2.members m2 where m2.age >= 20)

 

▶ 서브 쿼리 한계

  • JPA 표준 스펙: 서브 쿼리는 WHERE 및 HAVING 절에서만 사용할 수 있다.
  • Hibernate 구현체: 
    • Hibernate: SELECT 절에서도 서브 쿼리를 사용할 수 있다.
    • Hibernate 6: FROM 절에서도 서브 쿼리를 사용할 수 있다.
      • 이전 버전에서는 FROM 절에서 서브 쿼리를 사용할 수 없으며, 이 경우 조인으로 풀어서 해결해야 한다. (가능할 경우)

 

 

 

✅ 타입 표현

▶ 문자

  • 작은따옴표(')를 사용하여 표현한다.
    • 예: 'HELLO'
  • 작은따옴표(')는 두 번 연속 사용하여 표현한다.
    • 예: 'She''s'

🔽 예시 코드

  • username이 'HELLO'인 회원
select m from Member m where m.username = 'HELLO'

 

▶ 숫자

  • Long 타입: L 접미사를 사용한다. 
    • 예: 10L
  • Double 타입: D 접미사를 사용한다.
    • 예: 10D
  • Float 타입: F 접미사를 사용한다.
    • 예: 10F
  • 정수: 접미사 없이 사용한다.
    • 예: 10

🔽 예시 코드

  • 가격이 10L인 상품
select p from Product p where p.price = 10L

 

▶ Boolean

  • TRUE, FALSE

🔽 예시 코드

  • available이 true인 상품
select p from Product p where p.available = true

 

▶ ENUM

  • 패키지 이름을 포함한 전체 이름을 사용해야 한다.
    • 예: jpabook.MemberType.Admin

🔽 예시 코드

  • memberType이 Admin인 회원
select m from Member m where m.memberType = jpabook.MemberType.Admin

 

▶ 엔티티 타입

  • 엔티티 타입을 표현하며 주로 상속 관계에서 사용된다.
    • 예: TYPE(m) = Member (상속 관계에서 사용)

🔽 예시 코드

  • Item 엔티티와 상속 관계에 있는 Book 엔티티
select i from Item i where type(i) = Book

 

 

 

✅ 기타식

JPQL은 SQL과 문법이 같은 식을 지원한다.

 

▶ 서브 쿼리

  • [NOT] EXISTS: 서브 쿼리에 결과가 존재하면 참이다. NOT EXISTS는 결과가 없을 때 참이다.
  • [NOT] IN: 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참이다. NOT IN은 어느 것도 같은 것이 없을 때 참이다.

🔽 예시 코드

  • 주소 중 city가 New York인 주소가 하나라도 있는 사람
select p from Person p where exists (select a from p.address a where a.city = 'New York')
  • 나이가 25, 30, 35인 사람
select p from Person p where p.age in (25, 30, 35)
위의 서브 쿼리 부분을 참고하자.

 

▶ 논리 연산

  • AND: 두 조건을 만족하면 참이다.
  • OR: 두 조건 중 하나만 만족해도 참이다.
  • NOT: 조건식의 반대를 만족하면 참이다.

🔽 예시 코드

  • 나이가 25보다 많고, city가 New York인 사람
select p from Person p where p.age > 25 and p.city = 'New York'
  • city가 New York이 아닌 사람
select p from Person p where not p.city = 'New York'

 

▶ 비교 연산

  • 비교 연산자: =, >, >=, <, <=, <>

🔽 예시 코드

  • 나이가 30보다 큰 사람
select p from Person p where p.age > 30

 

▶ BETWEEN

  • BETWEEN: 특정 범위 내에 값이 있는지 확인한다. 범위의 양 끝 값을 포함한다.

🔽 예시 코드

  • 나이가 25와 35 사이(포함)에 있는 사람
select p from Person p where p.age between 25 and 35

 

▶ LIKE

  • LIKE: 문자열 패턴과 일치하는 값을 조회한다.

🔽 예시 코드

  • 이름이 J로 시작하는 모든 사람
select p from Person p where p.name like 'J%'

 

▶ IS [NOT] NULL

  • IS NULL: 값이 NULL 인지 확인한다.
  • IS NOT NULL: 값이 NULL이 아닌지 확인한다.

🔽 예시 코드

  • 전화번호가 null이 아닌 사람
select p from Person p where p.phoneNumber is not null

 

 

 

✅ 조건식 - CASE 식

JPQL은 SQL과 마찬가지로 특정 조건에 따라 분기할 때 CASE 식을 사용한다.

  • CASE 식의 4가지 종류
    • 기본 CASE
    • 단순 CASE
    • COALESCE
    • NULLIF

 

▶ 기본 CASE

여러 개의 조건식을 사용하여 각 조건이 참일 때 어떤 결과를 반환할지 정의한다.

select
    case when m.age <= 10 then '학생요금'
         when m.age >= 60 then '경로요금'
         else '일반요금'
    end
from Member m

 

▶ 단순 CASE

하나의 표현식을 기준으로 여러 조건을 비교하여 결과를 반환한다.

select
    case t.name
        when 'TeamA' then '인센티브110%'
        when 'TeamB' then '인센티브120%'
        else '인센티브105%'
    end
from Team t

 

▶ COALESCE

스칼라식을 차례대로 하나씩 조회해서 null이 아니면 반환한다.

select coalesce(m.username, '이름 없는 회원') from Member m
  • 설명: m.username이 null이면 '이름 없는 회원'을 반환한다.

 

▶NULLIF

두 값이 같으면 null을, 다르면 첫 번째 값을 반환한다.

select nullif(m.username, '관리자') from Member m
  • 설명: m.username이 '관리자'이면 null을 반환하고, null이 아니면 m.username을 반환한다.

 

 

 

✅ 기본 함수

▶ CONCAT

  • CONCAT(문자1, 문자2, ...): 문자를 합친다.

🔽 예시 코드

select concat('a', 'b') from Member m
  • 설명: 'a'와 'b'를 연결하여 'ab'를 반환한다.

 

▶ SUBSTRING

  • SUBSTRING(문자, 위치, [길이]): 위치부터 시작해 길이만큼 문자를 구한다.
    • 위치: 추출을 시작할 위치
    • 길이: 추출할 문자 수

🔽 예시 코드

select substring(m.username, 1, 3) from Member m
  • 설명: username이 'soeun'인 경우 'soe'를 반환한다.

 

▶TRIM

  • TRIM([ [ LEADING | TRAILING | BOTH ] [트림 문자] FROM ] 문자): 트림 문자를 제거한다.
    • LEADING: 문자열의 왼쪽 제거 
    • TRAILING: 문자열의 오른쪽 제거
    • BOTH: 문자열의 양쪽 제거 (기본값)
    • 트림 문자: 제거할 문자를 지정 (기본값은 공백 문자(스페이스))

🔽 예시 코드

select trim(both ' ' from m.username) from Member m
  • 설명: username이 ' soeun '인 경우 'soeun'을 반환한다.

 

▶ LOWER

  • LOWER(문자): 소문자로 변경한다.

🔽 예시 코드

select lower(m.username) from Member m
  • 설명: username이 'SOEUN'인 경우 'soeun'을 반환한다.

 

▶UPPER

  • UPPER(문자): 대문자로 변경한다.

🔽 예시 코드

select upper(m.username) from Member m
  • 설명: username이 'soeun'인 경우 'SOEUN'을 반환한다.

 

▶LENGTH

  • LENGTH(문자): 문자 길이를 구한다.

🔽 예시 코드

select length(m.username) from Member m
  • 설명: username이 'soeun'인 경우, 5를 반환한다.

 

▶ LOCATE

  • LOCATE(찾을 문자, 원본 문자, [검색 시작 위치]): 검색 위치부터 문자를 검색한다.
    • 찾을 문자: 검색할 문자열
    • 원본 문자: 검색 대상 문자열
    • 검색 시작 위치: 검색을 시작할 위치 (기본값은 1)

🔽 예시 코드

select locate('Soeun', m.username) from Member m
  • 설명: username이 'LeeSoeun'인 경우 4를 반환한다.

 

▶ ABS

  • ABS(수식): 절댓값으로 반환한다.

🔽 예시 코드

select abs(m.salary - 5000) from Member m
  • 설명: salary에서 4000이면 -1000의 절댓값인 1000을 반환한다.

 

▶ SQRT

  • SQRT(수식): 제곱근으로 변환한다.

🔽 예시 코드

select sqrt(m.salary) from Member m
  • 설명: salary가 1600이면 40을 반환한다.

 

▶MOD

  • MOD(수식, 나눌 수): 나머지를 구한다.

🔽 예시 코드

select mod(m.salary, 3) from Member m
  • 설명: salary가 10이면 1을 반환한다.

 

▶ SIZE

  • SIZE(컬렉션): 컬렉션의 크기(원소 개수)를 구한다.

🔽 예시 코드

select size(m.orders) from Member m

 

▶INDEX

  • INDEX(별칭): LIST 타입 컬렉션의 위치 값
    • 컬렉션이 @OrderColumn을 사용하는 LIST 타입일 때만 사용이 가능하다.

🔽 예시 코드

select index(t.members) from Team t
김영한 선생님께서는 쓰는 걸 추천하지 않는다고 하신다...

 

▶사용자 정의 함수

사용자 정의 함수를 Hibernate에서 사용하려면, 데이터베이스 방언을 상속받아 함수를 등록해야 한다.

방언(Dialect): Hibernate가 특정 데이터베이스와 상호 작용하는 방식을 정의하는 클래스

🔽 예시

예제: 'group_concat' 함수 등록

1. 방언 클래스 작성

public class MyH2Dialect extends H2Dialect {

    public MyH2Dialect() {
        // 'group_concat' 함수 등록
        registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
    }
}

2. Hibernate 설정 파일에서 방언 클래스 설정

<property name="hibernate.dialect" value="dialect.MyH2Dialect"/>

3. 사용자 정의 함수 호출

//JPQL에서 함수 호출
select function('group_concat', m.username) from Member m

//Hibernate 사용 시 
select group_concat(m.username) from Member m

 

 

 

📍 참고

soeun2537