App Engine에서 JPA 사용

JPA(Java Persistence API)는 자바에서 데이터베이스에 액세스하기 위한 표준 인터페이스로, 자바 클래스와 데이터베이스 테이블 간에 자동 매핑을 제공합니다. JPA를 Datastore와 함께 사용하기 위한 오픈소스 플러그인이 있으며 이 페이지는 해당 플러그인을 시작하는 방법에 대한 정보를 제공합니다.

경고: 대부분의 개발자는 낮은 수준의 Datastore API 또는 Datastore 전용으로 개발된 오픈소스 API 중 하나(예: Objectify)를 사용하는 것이 더 좋습니다. JPA는 기존 관계형 데이터베이스에 사용하도록 설계되었기 때문에 항목 그룹이나 상위 쿼리 등 관계형 데이터베이스와 차별화되는 Datastore의 몇 가지 측면을 명시적으로 표시할 수 있는 방법이 없습니다. 이로 인해 이해하고 해결하기에 어려운 미묘한 문제가 발생할 수 있습니다.

이 플러그인의 버전 1.x는 JPA 버전 1.0을 구현하는 App Engine 자바 SDK에 포함되어 있습니다. 구현은 DataNucleus Access Platform 버전 1.1을 기반으로 합니다.

참고: 이 페이지의 안내는 App Engine용 DataNucleus 플러그인의 버전 1.x를 사용하는 JPA 버전 1에 적용됩니다. JPA 2.0을 사용할 수 있도록 하는 DataNucleus 플러그인 버전 2.x도 사용할 수 있습니다. 2.x 플러그인은 다수의 새로운 API 및 기능을 제공하지만 업그레이드가 이전 버전인 1.x 버전과 완전히 호환되지는 않습니다. JPA 2.0을 사용하여 애플리케이션을 다시 빌드하는 경우에는 코드를 업데이트하고 다시 테스트해야 합니다. 새 버전에 대한 자세한 내용은 App Engine에서 JPA 2.0 사용을 참조하세요.

JPA 설정

JPA를 사용하여 데이터 저장소에 액세스하려면 App Engine 앱에 다음이 필요합니다.

  • JPA 및 Datastore JAR은 war/WEB-INF/lib/ 디렉터리에 있어야 합니다.
  • persistence.xml이라는 구성 파일이 앱의 war/WEB-INF/classes/META-INF/ 디렉터리에 있고, JPA가 App Engine Datastore를 사용하도록 구성되어 있어야 합니다.
  • 프로젝트의 빌드 프로세스는 컴파일된 데이터 클래스에서 컴파일 후 '보정' 단계를 수행하여 컴파일된 데이터를 JPA 구현과 연관시켜야 합니다.

JAR 복사

JPA 및 Datastore JAR은 App Engine Java SDK에 포함되어 있습니다. appengine-java-sdk/lib/user/orm/ 디렉터리에서 찾을 수 있습니다.

JAR을 애플리케이션의 war/WEB-INF/lib/ 디렉터리에 복사합니다.

appengine-api.jarwar/WEB-INF/lib/ 디렉터리에 있는지 확인합니다. 프로젝트를 만들 때 이미 복사했을 수도 있습니다. App Engine DataNucleus 플러그인은 이 JAR을 사용하여 Datastore에 액세스합니다.

persistence.xml 파일 만들기

JPA 인터페이스는 애플리케이션의 war/WEB-INF/classes/META-INF/ 디렉터리에 persistence.xml이라는 구성 파일을 필요로 합니다. 이 위치에서 이 파일을 직접 만들거나 빌드 프로세스를 통해 이 파일을 소스 디렉터리에서 복사할 수 있습니다.

다음과 같은 내용으로 파일을 만듭니다.

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="transactions-optional">
        <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </properties>
    </persistence-unit>

</persistence>

데이터 저장소 읽기 정책 및 호출 기한

Datastore 쿼리 페이지에 설명된 대로 persistence.xml 파일에서 EntityManagerFactory의 읽기 정책(strong consistency 또는 eventual consistency) 및 Datastore API 호출 기한을 설정할 수 있습니다. 이 설정은 <persistence-unit> 요소 안에 들어갑니다. 해당 EntityManager 인스턴스를 통해 이루어지는 모든 호출은 EntityManagerFactory에 의해 관리자가 생성되었을 때 선택된 구성을 사용합니다. 개별 Query에 대해 이 옵션을 재정의할 수도 있습니다(아래에 설명됨).

읽기 정책을 설정하려면 datanucleus.appengine.datastoreReadConsistency라는 속성을 포함하세요. 가능한 값은 EVENTUAL(eventual consistency를 사용하는 읽기의 경우) 및 STRONG(strong consistency를 사용하는 읽기의 경우)입니다. 지정되지 않은 경우 기본값은 STRONG입니다.

            <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />

읽기 및 쓰기에 대해 별도의 Datastore 호출 기한을 설정할 수 있습니다. 읽기의 경우 JPA 표준 속성 javax.persistence.query.timeout을 사용합니다. 쓰기의 경우 datanucleus.datastoreWriteTimeout을 사용합니다. 값은 밀리초 단위의 시간입니다.

            <property name="javax.persistence.query.timeout" value="5000" />
            <property name="datanucleus.datastoreWriteTimeout" value="10000" />

XG(교차 그룹) 트랜잭션을 사용하려면 다음 속성을 추가합니다.

            <property name="datanucleus.appengine.datastoreEnableXGTransactions" value="true" />

같은 persistence.xml 파일에서 서로 다른 name 속성을 사용하는 여러 개의 <persistence-unit> 요소가 같은 앱에서 서로 다른 구성으로 EntityManager 인스턴스를 사용하도록 할 수 있습니다. 예를 들어 다음 persistence.xml 파일은 두 개의 구성 세트를 설정합니다. 하나는 "transactions-optional"이고, 다른 하나는 "eventual-reads-short-deadlines"입니다.

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="transactions-optional">
        <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </properties>
    </persistence-unit>

    <persistence-unit name="eventual-reads-short-deadlines">
        <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>

            <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />
            <property name="javax.persistence.query.timeout" value="5000" />
            <property name="datanucleus.datastoreWriteTimeout" value="10000" />
        </properties>
    </persistence-unit>
</persistence>

명명된 구성 세트로 EntityManager를 만드는 방법은 아래의 EntityManager 인스턴스 가져오기를 참조하세요.

개별 Query 객체의 읽기 정책 및 호출 기한을 재정의할 수 있습니다. Query의 읽기 정책을 재정의하려면 다음과 같이 해당 setHint() 메서드를 호출합니다.

        Query q = em.createQuery("select from " + Book.class.getName());
        q.setHint("datanucleus.appengine.datastoreReadConsistency", "EVENTUAL");

위에서처럼 가능한 값은 "EVENTUAL""STRONG"입니다.

읽기 제한시간을 재정의하려면 다음과 같이 setHint()를 호출합니다.

        q.setHint("javax.persistence.query.timeout", 3000);

키를 사용하여 항목을 가져올 때는 이 옵션의 구성을 재정의할 수 있는 방법이 없습니다.

데이터 클래스 향상

JPA의 DataNucleus 구현은 빌드 프로세스에서 컴파일 후 '향상' 단계를 사용하여 데이터 클래스를 JPA 구현과 연관시킵니다.

명령줄에서 다음 명령어를 사용하여 컴파일된 클래스에서 향상 단계를 수행할 수 있습니다.

java -cp classpath org.datanucleus.enhancer.DataNucleusEnhancer
class-files

classpathappengine-java-sdk/lib/tools/ 디렉터리의 JAR datanucleus-core-*.jar, datanucleus-jpa-*, datanucleus-enhancer-*.jar, asm-*.jar, geronimo-jpa-*.jar(*는 각 JAR의 해당 버전 번호)와 모든 데이터 클래스를 포함해야 합니다.

DataNucleus 바이트코드 보정기에 대한 자세한 내용은 DataNucleus 문서를 참조하세요.

EntityManager 인스턴스 가져오기

앱은 EntityManager 클래스의 인스턴스를 사용하여 JPA와 상호작용합니다. EntityManagerFactory 클래스의 인스턴스에서 메서드를 인스턴스화하고 호출함으로써 이 인스턴스를 가져옵니다. 팩토리는 JPA 구성("transactions-optional" 이름으로 식별됨)을 사용하여 EntityManager 인스턴스를 만듭니다.

EntityManagerFactory 인스턴스는 초기화되는 데 시간이 걸리므로 단일 인스턴스를 최대한 많이 재사용하는 것이 좋습니다. 쉬운 방법은 다음과 같이 정적 인스턴스가 있는 싱글톤 래퍼 클래스를 만드는 것입니다.

EMF.java

import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public final class EMF {
    private static final EntityManagerFactory emfInstance =
        Persistence.createEntityManagerFactory("transactions-optional");

    private EMF() {}

    public static EntityManagerFactory get() {
        return emfInstance;
    }
}

팁: "transactions-optional"persistence.xml 파일에 있는 구성 세트의 이름을 나타냅니다. 앱에서 여러 구성 세트를 사용하는 경우에는 원하는 Persistence.createEntityManagerFactory()를 호출하도록 이 코드를 확장해야 합니다. 코드는 각 EntityManagerFactory의 싱글톤 인스턴스를 캐시해야 합니다.

앱은 팩토리 인스턴스를 사용하여 Datastore에 액세스하는 요청마다 하나의 EntityManager 인스턴스를 만듭니다.

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;

import EMF;

// ...
    EntityManager em = EMF.get().createEntityManager();

EntityManager를 사용하여 데이터 객체를 저장, 업데이트, 삭제하고 Datastore 쿼리를 수행합니다.

EntityManager인스턴스 사용을 마쳤으면 이 인스턴스의 close() 메서드를 호출해야 합니다. close() 메서드를 호출한 후에 EntityManager 인스턴스를 사용하면 오류가 발생합니다.

    try {
        // ... do stuff with em ...
    } finally {
        em.close();
    }

클래스 및 필드 주석

JPA에서 저장되는 각 객체는 App Engine Datastore에서 하나의 항목이 됩니다. 항목의 종류는 패키지 이름 없이 클래스의 단순 이름에서 파생됩니다. 클래스의 각 영구 필드는 항목 속성을 나타내고 속성 이름은 필드 이름과 같으며 대소문자는 유지됩니다.

JPA를 통해 Datastore에서 저장 및 검색할 수 있도록 자바 클래스를 선언하려면 클래스에 @Entity 주석을 지정합니다. 예를 들면 다음과 같습니다.

import javax.persistence.Entity;

@Entity
public class Employee {
    // ...
}

데이터 저장소에 저장되는 데이터 클래스 필드는 기본적으로 영구적인 유형이거나 명시적으로 영구 필드로 선언되어야 합니다. DataNucleus 웹사이트에서 JPA 기본 지속성 동작이 자세히 설명된 차트를 볼 수 있습니다. 명시적으로 영구 필드로 선언하려면 @Basic 주석을 붙이면 됩니다.

import java.util.Date;
import javax.persistence.Enumerated;

import com.google.appengine.api.datastore.ShortBlob;

// ...
    @Basic
    private ShortBlob data;

필드 유형은 다음 중 하나일 수 있습니다.

  • 데이터 저장소에서 지원하는 핵심 유형 중 하나
  • 핵심 Datastore 유형의 값 컬렉션(예: java.util.List<...>)
  • @Entity 클래스의 인스턴스 또는 인스턴스 컬렉션
  • 항목에 속성으로 저장되는 포함된 클래스

데이터 클래스에는 공개 또는 보호된 기본 생성자 그리고 해당 데이터 저장소 항목의 기본 키를 저장하는 전용 필드가 하나 있어야 합니다. 각각 다른 값 유형과 주석을 사용하는 네 가지 종류의 키 필드 중에서 한 필드를 선택할 수 있습니다. (자세한 내용은 데이터 만들기: 키를 참조하세요.) 가장 간단한 키 필드는 객체가 처음으로 Datastore에 저장될 때 JPA가 모든 클래스 인스턴스에 걸쳐 고유한 값을 자동으로 채우는 긴 정수 값입니다. 긴 정수 키는 @Id 주석과 @GeneratedValue(strategy = GenerationType.IDENTITY) 주석을 사용합니다.

import com.google.appengine.api.datastore.Key;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

// ...
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

다음은 데이터 클래스의 예시입니다.

import com.google.appengine.api.datastore.Key;

import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

    private String firstName;

    private String lastName;

    private Date hireDate;

    // Accessors for the fields. JPA doesn't use these, but your application
    does.

    public Key getKey() {
        return key;
    }

    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Date getHireDate() {
        return hireDate;
    }
    public void setHireDate(Date hireDate) {
        this.hireDate = hireDate;
    }
}

상속

JPA는 상속을 사용하는 데이터 클래스 생성을 지원합니다. App Engine에서 JPA 상속이 어떻게 작동하는지 알아보기 전에 이 주제에 대한 DataNucleus 문서를 먼저 읽어보는 것이 좋습니다. 다 읽어보셨나요? 확인. App Engine에서 JPA 상속은 DataNucleus 문서에서의 설명과 같이 작동하지만 몇 가지 추가적인 제한사항이 있습니다. 이러한 제한사항을 설명한 후에 구체적인 예를 살펴보겠습니다.

'JOINED' 상속 전략을 사용하면 여러 '테이블'로 단일 데이터 객체의 데이터를 분할할 수 있습니다. 하지만 App Engine Datastore는 조인을 지원하지 않으므로, 이 상속 전략을 사용하여 데이터 객체를 조작하려면 각 상속 레벨의 리모트 프러시저 콜이 필요합니다. 이는 잠재적으로 매우 비효율적이므로 데이터 클래스에서는 'JOINED' 상속 전략이 지원되지 않습니다.

둘째, 'SINGLE_TABLE' 상속 전략을 사용하면 상속 계층구조의 루트에 있는 영구 클래스와 연관된 단일 '테이블'에 데이터 객체의 데이터를 저장할 수 있습니다. 이 전략에는 내재된 비효율성이 없지만 현재 지원되지 않습니다. 이 부분은 향후 출시 버전에서 다시 논의할 예정입니다.

이제 좋은 소식을 살펴보겠습니다. 'TABLE_PER_CLASS' 및 'MAPPED_SUPERCLASS' 전략은 DataNucleus 문서의 설명대로 작동합니다. 예를 살펴보겠습니다.

Worker.java

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

@Entity
@MappedSuperclass
public abstract class Worker {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

    private String department;
}

Employee.java

// ... imports ...

@Entity
public class Employee extends Worker {
    private int salary;
}

Intern.java

import java.util.Date;
// ... imports ...

@Entity
public class Intern extends Worker {
    private Date internshipEndDate;
}

이 예시에서는 Worker 클래스 선언에 @MappedSuperclass 주석을 추가했습니다. 이는 하위 클래스의 Datastore 항목에 Worker의 모든 영구 필드를 저장하도록 JPA에 지시합니다. Employee 인스턴스를 포함한 persist()를 호출한 결과로 만들어진 Datastore 항목에는 'department'와 'salary'라는 속성 두 개가 있습니다. Intern 인스턴스를 포함한 persist()를 호출한 결과로 만들어진 Datastore 항목에는 'department'와 'inernshipEndDate'라는 속성 두 개가 있습니다. Datastore에 'Worker' 종류의 항목은 없습니다.

이제 더 흥미로운 내용을 살펴보겠습니다. EmployeeIntern 외에, 퇴사한 직원을 설명하는 Employee의 전문 분야도 필요하다고 가정해보겠습니다.

FormerEmployee.java

import java.util.Date;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
// ... imports ...

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class FormerEmployee extends Employee {
    private Date lastDay;
}

이 예시에서는 strategy 속성은 InheritanceType.TABLE_PER_CLASS로 설정하여 FormerEmployee 클래스 선언에 @Inheritance 주석을 추가했습니다. 이는 JPA에 FormerEmployee의 모든 영구 필드와 슈퍼클래스를 FormerEmployee 인스턴스에 해당하는 Datastore 항목에 저장하도록 지시합니다. FormerEmployee 인스턴스를 포함한 persist()를 호출한 결과로 만들어진 Datastore 항목에는 'department', 'salary', 'lastDay'라는 속성 세 개가 있습니다. FormerEmployee에 해당하는 'Employee' 종류의 항목은 없지만 런타임 유형이 Employee인 객체를 포함한 persist()를 호출하면 'Employee' 종류의 항목이 생성됩니다.

선언된 관계 필드의 유형이 해당 필드에 할당되는 객체의 런타임 유형과 일치하면 관계와 상속이 혼합되어도 문제가 없습니다. 자세한 내용은 다형성 관계 섹션을 참조하세요. 이 섹션에는 JDO 예가 나와 있지만 개념과 제한사항은 JPA에서도 동일합니다.

JPA 1.0의 지원되지 않는 기능

JPA 인터페이스의 다음 기능은 App Engine 구현에서 지원되지 않습니다.

  • 다대다 소유 관계 및 미소유 관계. 명시적 Key 값을 사용하여 미소유 관계를 구현할 수는 있지만 API에서는 유형 확인이 적용되지 않습니다.
  • 'Join' 쿼리. 상위 종류에 대한 쿼리를 수행할 때 필터에서 하위 항목의 필드를 사용할 수 없습니다. 쿼리에서 키를 사용하여 상위 항목의 관계 필드를 직접 테스트할 수 있다는 점을 참고하세요.
  • 집계 쿼리(group by, having, sum, avg, max, min)
  • 다형성 쿼리. 클래스 쿼리를 실행하여 서브클래스의 인스턴스를 가져올 수 없습니다. 각 클래스는 Datastore에서 별도의 항목 종류로 표시됩니다.