Post

Springboot Jpa - 객체지향 쿼리 언어

JPQL에 대해 알아보고 간단한 사용법을 익혀보자.

Springboot Jpa - 객체지향 쿼리 언어

객체지향 쿼리 언어란?

JPA는 다양한 쿼리 방법을 지원하는데, 어떤 종류가 있는지 알아보자.

종류

JPQL
JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공한다. SQL을 추상화 한 것으로 특정 DB SQL에 의존적이지 않다.
SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원한다.
JPQL은 엔티티 객체를 대상으로 쿼리하고, SQL은 데이터베이스 테이블을 대상으로 쿼리한다. 실제 동작은 JPQL을 작성하게 되면, JPQL이 SQL로 변환되어 실행된다.
JPA를 사용하면 엔티티 객체를 중심으로 개발해야하고 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색해야하는데, 모든 데이터 DB를 객체로 변환해서 검색하는 것은 불가능 하기 때문에 사용된다.
JPA Criteria
java 코드로 작성했을 때, JPQL 코드로 빌드해주는 Generator의 클래스 모음
JPQL에서 해결하기에 복잡한 동적쿼리에 대한 생성을 보안하기 위해 제공되는 언어다.
JPA 공식 기능이다.
너무 복잡하고 실용성이 없기 때문에, 같은 기능의 QueryDSL 을 사용하는 것을 권장한다.
QueryDSL
java 코드로 작성했을 때, JPQL 코드로 빌드해주는 Generator의 클래스 모음
JPA Criteria와 같이 JPQL에서 해결하기 복잡한 동적쿼리에 대한 생성을 보안하기 위해 제공되는 언어다.
단순하고 쉬워서 실무에 사용하기에 좋다.
네이티브 SQL
특정 데이터베이스 벤더에 종속적이여야하는 쿼리를 직접 작성한다.
JPQL로 해결할 수 없는 특정 DB에 의존적인 기능들을 사용할 때 이용된다.
JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용
JPA를 사용하면서 JDBC, JdbcTemplate, MyBatis 등을 함께 사용할 수 있다.
주의점은 영속성 컨텍스트를 적절한 시점에 강제로 플러시를 필요로 한다. JPQL을 사용하거나 native query를 이용하면 알아서 flush되는데, 이외의 방법으로 DB 조회시에는 그 직전에 flush 되도록 해야한다.

JPQL(Java Persistence Query Language)이란?

JPQL
JPQL은 객체지향 쿼리 언어다.따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
JPQL은 결국 SQL로 변환된다.

기본 문법

1
2
3
4
5
6
7
8
9
10
select_문 :: =
  select_절
  from_절
  [where_절]
  [groupby_절]
  [having_절]
  [orderby_절]
  
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]

JPQL 문법 구조

select m from Member as m where m.age > 18

  • JPQL 키워드(SELECT, FROM, where)는 대소문자를 구분하지 않고, 엔티티와 속성(Member, age)은 대소문자 구분한다.
  • 테이블 이름이 아닌, 엔티티 이름 사용한다.
  • 별칭(m)은 필수지만, AS는 생략할 수 있다.

API

집합과 정렬 관련 API

  • COUNT(m)
  • SUM(m.age)
  • AVG(m.age)
  • MAX(m.age)
  • MIN(m.age)
  • GROUP BY
  • HAVING
  • ORDER BY

반환 타입

  • TypeQuery: 반환 타입이 명확할 때 사용
  • Query: 반환 타입이 명확하지 않을 때 사용

결과 조회 API

  • query.getResultList(): 결과가 하나 이상일 때 사용
    • 결과가 하나 이상일 때 리스트 반환
    • 결과가 없으면 빈 리스트 반환
  • query.getSingleResult(): 결과가 정확히 하나일 떄 사용
    • 결과가 하나 일 때 단일 객체 반환
    • 결과가 없으면: javax.persistence.NoResultException
    • 둘 이상이면: javax.persistence.NonUniqueResultException

파라미터 바인딩

  • 이름 기준 SELECT m FROM Member m where m.username=:username SELECT m FROM Member m where m.username=:[이름]
    • 값 세팅: query.setParameter("username", usernameParam);
  • 위치 기준 SELECT m FROM Member m where m.username=?1 SELECT m FROM Member m where m.username=?[위치]
    • 갑 세팅: query.setParameter(1, usernameParam);

프로젝션(SELECT)

프로젝션
SELECT 절에 조회할 대상을 지정하는 것
대상 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입) 관계형 DB에서는 스칼라 타입만 대상이 된다.

예제

  • 엔티티 프로젝션: SELECT m FROM Member m, SELECT m.team FROM Member m
    • SELECT m.team FROM Member m은 실제로 Join 되어 나가기 때문에, join 쿼리는 명시적으로 join을 적어주는게 좋다.
  • 임베디드 타입 프로젝션: SELECT m.address FROM Member m
  • 스칼라 타입 프로젝션: SELECT m.username, m.age FROM Member m

여러 값 조회

단일 값이 아니라, 여러개의 값을 조회하고 싶다면 어떻게 해야할까?

예시: SELECT m.username, m.age FROM Member m

  • Query 타입으로 조회
    • List resultList = em.createQuery("[쿼리]");
      • 타입을 지정하지 않으면 Object[] 타입으로 결과가 조회된다.
  • Object[] 타입으로 조회
    • List<Object[]> resultList = em.createQuery("[쿼리]");
    • 단순히 위에 방식에서 타입 Object[]로의 타입 캐스팅만 되어있는 방식이다.
  • new 명령어로 조회
    • SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
    • 패키지 명을 포함한 전체 클래스 명을 입력해야하고, DTO에 순서와 타입이 일치하는 생성자가 필요하다.

페이징

JPA는 페이징을 다음 두 API로 추상화되어있고, 사용하면 알아서 각 DB 방언에 맞춰서 변환된다.

1
2
3
4
5
String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
                            .setFirstResult(10)
                            .setMaxResults(20)
                            .getResultList();
  • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수

조인

  • 내부 조인: SELECT m FROM Member m [INNER] JOIN m.team t
  • 외부 조인: SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
  • 세타 조인: SELECT count(m) FROM Member m, Team t WHERE m.username = t.name

ON 절

JPA 2.1부터 ON 절을 활용한 조인이 지원된다.

ON 절을 이용한 조인으로 할 수 있는 것

  • 조인 대상 필터링
  • 연관관계 없는 엔티티 외부 조인(하이버네이트 5.1부터)

조인 대상 필터링

Member와 Team을 조인하는데, 이때 팀은 name이 ‘A’인 애들만 조회하고싶어

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

서브쿼리

  • [NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
    • {ALL | ANY | SOME} (subquery)
    • ALL: 모두 만족하면 참
    • ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
  • [NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참

제약조건

  • JPA 표준스펙: WHERE, HAVING 절에서만 서브 쿼리 사용 가능
  • Hibernate: SELECT와 FROM 절(6 버전 이상)에서 사용 가능

JPQL 타입 표현과 기타식

타입 표현

  • 문자: ‘HELLO’, ‘She’’s’
    • 싱글쿼테이션(') 을 사용해야하는 경우에는 2번 쓰면 된다.
  • 숫자: 10L-Long, 10D - Double, 10F - Float
  • Boolean: TRUE, FALSE
  • ENUM: jpabook.MemberType.Admin
    • 패키지명 포함해서 적어야한다.
  • 엔티티 타입: TYPE(m) = Member
    • 상속 관계에서 사용된다.

기타 표현식

SQL과 문법이 같은 식들을 의미한다.

  • EXISTS, IN
  • AND, OR, NOT
  • =, >, >=, <, <=, <>
  • BETWEEN, LIKE, IS NULL

조건식 (CASE 등)

  • 기본 CASE 식: 범위가 맞다면
    1
    2
    3
    4
    5
    6
    
    select
      case when m.age <= 10 then '학생요금'
           when m.age >= 60 then '경로요금'
           else '일반요금'
      end
    from Member m
    
  • 단순 CASE 식: 값이 딱 맞는경우라면
    1
    2
    3
    4
    5
    6
    7
    
    select
      case t.name
          when '팀A' then '인센티브110%'
          when '팀B' then '인센티브120%'
          else '인센티브105%'
      end
    from Team t
    
  • COALESCE: 하나씩 조회해서 null이 아니면 반환
    1
    
    select coalesce(m.username,'이름 없는 회원') from Member m
    
  • NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
    1
    
    select NULLIF(m.username, '관리자') from Member m
    

JPQL 함수

기본 함수

JPA 표준에서 지원하는 함수

  • CONCAT: 문자열 연결
  • SUBSTRING: 문자열 자르기
  • TRIM: 문자열 앞 뒤 공백 제거
  • LOWER, UPPER: 문제열 대소문자 변환
  • LENGTH: 문자열 길이
  • LOCATE: 문자열에서 특정 문자열이 시작하는 위치
  • ABS, SQRT, MOD: 수학 관련 메서드
  • SIZE: 엔티티 내의 다른 엔티티의 크기
  • INDEX(JPA 용도): List 내에 해당 엔티티의 index

사용자 정의 함수

사용전에 방언(H2Dialect 등)에 추가해야 한다.

  • 사용자 방언에 추가
    1
    2
    3
    4
    5
    
    publci class MyH2Dialect extends H2Dialect{
      public MyH2Dialect() {
        registerFunction("[이름]", new StandardSQLFunction("[이름]", StandardBasicType.STRING))
      }
    }
    
  • 사용자 방언을 사용하도록 persistence.xml 변경

경로 표현식

경로 표현식
.(점)을 이용해 객체 그래프를 탐색하는 것
1
2
3
4
5
6
7
8
9
10
SELECT
    m.username  // 상태 필드
FROM
    Member m    
JOIN 
    m.team t    // 단일 값 연관 필드
JOIN 
    m.orders o  // 컬렉션 값 연관 필드
WHERE
    t.name = ' A'

종류

  • 상태 필드: 단순히 값을 저장하기 위한 필드
  • 단일 값 연관 필드: 연관관계를 위한 필드로 단일 값 연관 필드(ManyToOne, OneToOne), 대상이 엔티티
  • 컬렉션 값 연관 필드: 연관관계를 위한 필드로 컬렉션 값 연관 필드(OneToMany, ManyToMany), 대상이 컬렉션

특징

  • 상태 필드(state field): 경로 탐색의 끝, 탐색X
  • 단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색O
  • 컬렉션 값 연관 경로: 묵시적 내부 조인 발생, 탐색X, FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능

명시적 조인과 묵시적 조인

명시적 조인
join 키워드 직접 사용
select m from Member m join m.team t
묵시적 조인
경로 표현식에 의해 묵시적으로 SQL 조인 발생(내부 조인만 가능)
select m.team from Member m

경로 탐색을 사용한 묵시적 조인 시 주의사항

  • 묵시적 조인은 항상 내부 조인
  • 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야함
  • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 줌
  • 조인은 SQL 튜닝에 중요 포인트인데, 묵시적 조인은 조인이 발생하는 상황을 한 눈에 파악하기 어려우니 가급적 묵시적 조인 대신에 명시적 조인 사용

페치 조인(fetch join)

페치 조인(fetch join)
SQL 조인 종류가 아닌, JPQL에서 성능 최적화를 위해 제공하는 기능이다.
연관된 엔티티나 컬렉션을 한번의 SQL로 함께 조회하는 기능이다.
join fetch 명령어를 이용해 사용한다. [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로

엔티티 페치 조인

엔티티 페치 조인에 대해 알아보자. 엔티티 페치 조인은 SQL 한 번으로 회원을 조회 + 연관된 팀 조회하도록 하는 조인이다.

DB 구조와 데이터 DB 구조와 데이터

얻고 싶은 조회 결과 얻고 싶은 조회 결과


1
2
3
4
5
SELECT 
  m
FROM 
  Member m 
JOIN FETCH m.team
1
2
3
4
5
6
SELECT 
  M.*, T.* 
FROM 
  MEMBER M
INNER JOIN TEAM T 
ON M.TEAM_ID=T.ID

왼쪽이 fetch join을 사용해 작성한 JPQL이고, 오른쪽이 JPQL이 SQL로 바뀐 결과다.

fetch join으로 조회한 결과 fetch join으로 조회한 결과

비슷한 경우가 생각날 수 있는데, 즉시로딩으로 조회된 결과와 비슷하다. 즉시로딩과의 차이는 명시적으로 어떤 객체 그래프를 한번에 조인할 것인지에 대해 동적으로 정의했다는 점이 다르다.

이 쿼리를 지연로딩이나 즉시조인으로(=fetch join을 사용하지 않고) 조회했다면, 처음 조회에 각 멤버만 조회하고 멤버의 팀에 실제로 조회한다면 각 팀을 한번씩 조회하게 된다.

  1. 멤버 조회, 쿼리 실행
  2. 회원 1의 팀 조회, 쿼리 실행
  3. 회원 2의 팀 조회, 1차캐시에서 조회
  4. 회원 3의 팀 조회, 쿼리 실행

그렇게 되면 위와같이 4번, 최악의 경우 회원의 수만큼을 추가로 쿼리 실행을 하게 된다. 이를 N+1 문제라고 한다. N은 멤버의 각 팀을 조회하는 쿼리를 N번 실행하는 것을, +1은 처음 멤버를 조회하는 것 같은 N개의 결과를 조회하는 쿼리를 1번 실행하는 것을 의미한다.

컬렉션 페치 조인

이번엔 컬렉션 페치 조인에 대해 알아보자.

DB 구조와 데이터 DB 구조와 데이터

얻고 싶은 조회 결과 얻고 싶은 조회 결과


1
2
3
4
5
6
SELECT 
  t
FROM 
  Team t 
JOIN FETCH t.members
WHERE t.name = '팀A'
1
2
3
4
5
6
7
SELECT 
  T.*, M.*
FROM 
  TEAM T
INNER JOIN MEMBER M 
ON T.ID = M.TEAM_ID
WHERE T.NAME='팀A'

왼쪽이 fetch join을 사용해 작성한 JPQL이고, 오른쪽이 JPQL이 SQL로 바뀐 결과다.

fetch join으로 조회한 결과 fetch join으로 조회한 결과

결과를 보면, 회원의 수에 맞춰 팀A는 1개의 데이터 임에도 2번 출력된다. JPA는 그 결과를 컬렉션에 그대로 넣기 때문에, 결과의 사이즈가 2개여야하지만 3개로 출력되며 Team을 기준으로 봤을 때에는 중복이 발생한 것처럼 보인다. 그래서 [얻고 싶은 결과] 그림에서 teams결과 리스트가 2개로 표시된 것이다.

Hibernate6의 변경사항 원래는 위의 경우에서 1개의 팀이 출력되어야하는 결과에서 같은 데이터로 2번 출력된다고 했다. 그리고 그런 상황에서 중복을 제거하기 위해서는 DISTINCT라는 키워드를 사용해야 했는데, Hibernate6부터는 사용하지 않아도 중복이 제거되어 조회된다.

DISTINCT 사용시 DISTINCT 사용시

페치 조인과 일반 조인의 차이

  • 일반 조인은 실행시 연관된 엔티티를 함께 조회하지 않고, SELECT 절에 지정한 엔티티만 조회한다.
  • 페치 조인은 즉시 로딩처럼 연관된 엔티티도 함께 조회한다. 그렇지만 즉시 로딩은 N+1 문제를 발생시킨다.

페치 조인의 특징과 한계

페치 조인 대상에는 별칭을 줄 수 없다.
JPA에서 페치 조인을 설계할 때 목적은 그래프 탐색을 위한 것으로, 그래프 탐색은 전체 데이터가 조회되어야하는 것으로 데이터 정합성과 관련된 이슈가 발생할 수 있다. 그러므로 hibernate는 가능하긴 하지만, 사용하지 않는 것이 관례니 가급적 사용하지말자. 만약 별칭을 주고 별칭을 이용해 where 절에서 사용하며 특정 데이터만 가져오고싶은 로직 작성을 하려면, 아예 따로 조회하는것이 맞다.
둘 이상의 컬렉션은 페치 조인 할 수 없다.
조회하는 엔티티에 컬렉션 엔티티가 2개 이상 있을때, 모든 데이터를 한번에 다 페치조인으로 조회할 수는 없다는 의미다. 이미 컬렉션 엔티티를 조회시에 데이터가 중복으로 나오는데, 이런 컬렉션 엔티티를 여러개를 한번에 조회한다면 더욱 늘어날 것이다. 그렇게되면 데이터 정합성 이슈가 발생할 수 있기 때문에 둘 이상의 컬렉션은 페치 조인할 수 없다.
컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
컬렉션 페치 조인시, 중복데이터가 발생되기 때문에 정확한 페이징을 적용할 수 없기 때문에 사용할 수 없다.
만약 필요한 상황이라면, 위에 예시에서는 TEAM이 아닌 MEMBER를 중심으로 조회하고 페이징을 적용하는 방법이 있다. = 1:N 상황이 아닌 역으로 N:1 에서 페이징을 조회하는 방법
Batch Size를 이용한 방법도 있다. Team.members에 @BatchSize(size = [한번에 조회할 수])를 지정하고, team을 조회하며 페이징을 적용한다. 결과는 페이징은 team을 기준으로 되고, 지연 로딩으로 조회하기로 했던 members를 team 결과에 대해 한번에 조회할 수만큼 조회해온다. 이 @BatchSize는 글로벌로 지정할 수 있다.
엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한다.
페치 조인은 글로벌 로딩 전략 = @OneToMany(fetch = FetchType.LAZY)보다 우선 한다.
연관된 엔티티들을 SQL 한 번으로 조회하기 때문에 성능을 최적화 한다.
지연로딩, 즉시로딩은 N+1 문제를 발생시킨다.
실무에서 글로벌 로딩 전략은 모두 지연로딩으로 유지하고, 최적화가 필요한 곳에서는 페치 조인을 사용한다.
모든 것을 페치 조인으로 해결할 수는 없다.
객체 그래프를 유지할 때 사용하면 효과적이지만, 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 다른 결과를 내야할때는 일반 조인을 사용해 DTO로 반환하는 것이 효과적이다.

다형성 쿼리

예시 상황

위와 같은 상황에서, 다형성을 이용해 쿼리하는 것을 다형성 쿼리라 한다.

1
2
3
4
5
SELECT 
  i
FROM 
  Item i 
WHERE type(i) IN (Book, Movie)
1
2
3
4
5
SELECT
  i.*
FROM 
  ITEM i
WHERE i.DTYPE in ('B', 'M')

이와같이 사용할 수 있다.

TREAT

상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용할 수 있는 기능으로, 자바의 타입 캐스팅과 유사하다.

1
2
3
4
5
SELECT 
  i
FROM 
  Item i 
WHERE treat(i as Book).author = 'kim'
1
2
3
4
5
6
SELECT 
  i.*
FROM 
  ITEM i
WHERE i.DTYPE in 'B'
    AND i.author = 'kim'

이와같이 다운캐스팅을 하여 사용하는 것 비슷하게 동작하게 해준다.

엔티티 직접 사용

기본 키 값

JPQL에서 엔티티를 직접 사용하면, SQL에서 해당 엔티티의 기본 키 값을 사용하게 된다.

1
2
3
4
SELECT 
  count(m)
FROM 
  Member m
1
2
3
4
SELECT 
  count(m.id)
FROM 
  MEMBER m

JPQL위의 예제에서 count(m)으로 객체를 카운트 하도록 했지만, 실제는 기본키인 id를 카운트한다. 위와 같이 동작하는 이유는 객체를 식별하는 값은 기본키 이기 때문에 자동으로 변환된다. 만약, JPQL에서 m.id를 카운트 한다고 해도 똑같은 SQL로 변환된다. 또, 파라미터로 전달할 때에도 똑같이 적용된다.

외래 키 값

m의 연관관계가 있는 다른 엔티티를 객체로 접근한다면, 다른 객체의 기본키인 외래키를 사용하게 된다.

1
2
3
4
5
6
SELECT 
  m
FROM 
  Member m
WHERE
  m.team = :team
1
2
3
4
5
6
SELECT 
  m.*
FROM 
  MEMBER m
WHERE
  m.team_id = ?

Named 쿼리

Named 쿼리는 특정 쿼리에 이름을 부여하는 쿼리를 말한다.

1
2
3
4
5
6
7
@Entity
@NamedQuery(
    name = "Member.findByUsername",
    query="select m from Member m where m.username = :username")
public class Member {
    // 생략
}
1
2
3
4
List<Member> resultList =
    em.createNamedQuery("Member.findByUsername", Member.class)
    .setParameter("username","회원1")
    .getResultList();

왼쪽과같이 Entity 정의시 특정 쿼리에 대해 이름을 지정하고, 오른쪽과 같이 이름을 지정한 쿼리를 사용하면된다.

특징

  • Entity 정의시 외에도 XML에도 정의할 수 있다.
    META-INF/[파일명].xml에 정의하고, persistence.xml에 설정해서 사용한다.
    XML이 항상 우선권을 가지며, 운영환경에 따라 다른 XML을 배포할 수 있다.
  • 정적 쿼리로만 사용할 수 있다.
  • 애플리케이션 로딩 시점에 초기화 후 재사용된다.
    로딩 시점에 초기화 하기 때문에, 애플리케이션 로딩 시점에 쿼리를 검증한다.
  • @Query로 등록하는 쿼리들도, 이름이 없는 Named 쿼리다.

벌크 연산

여러 개의 데이터에 일괄로 똑같은 변경을 해야하는 상황일 때, JPA의 변경 감지로 실행한다면 각 데이터의 수만큼 쿼리가 실행될 것이다. 이럴 때 하나의 쿼리로 전체 적용하는 기능을 벌크 연산이라고 한다. JPA는 단건 연산에 최적화 되어있다.

.executeUpdate(), .executeDelete()로 실행하며, update와 delete를 지원한다.

Hibernate는 insert into ... select도 지원한다.

벌크 연산 주의

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다.

해결책

  • 벌크 연산을 먼저 실행한다.
  • 벌크 연산 수행 후 영속성 컨텍스를 초기화하고 새로 DB에서 조회해 사용한다.
This post is licensed under CC BY 4.0 by the author.