JDO의 항목 관계

객체 유형의 필드를 사용하여 영구 객체 간의 관계를 모델링할 수 있습니다. 영구 객체 간의 관계를 소유 관계로 설명할 경우, 두 객체 중 하나가 존재하지 않으면 다른 객체가 존재할 수 없습니다. 또는 영구 객체 간의 관계를 미소유 관계로 설명할 경우, 두 객체가 서로 독립적으로 존재할 수 있습니다. App Engine으로 구현된 JDO 인터페이스는 소유 및 미소유의, 단방향 및 양방향의 일대일 관계와 일대다 관계를 모두 모델링할 수 있습니다.

미소유 관계는 App Engine용 DataNucleus 플러그인 버전 1.0에서 지원되지 않지만, 필드에 직접 datastore 키를 저장하여 이러한 관계를 관리할 수 있습니다. App Engine은 관련된 항목들을 항목 그룹으로 자동으로 만들어 관련된 객체들이 함께 업데이트되도록 지원하지만, 앱이 데이터 저장소 트랜잭션을 사용할 시기를 파악해야 합니다

App Engine용 DataNucleus 플러그인 버전 2.x는 자연 구문을 통해 미소유 관계를 지원합니다. 미소유 관계 섹션은 각 플러그인 버전에서 미소유 관계를 만드는 방법을 자세히 설명합니다. App Engine용 DataNucleus 플러그인 버전 2.x로 업그레이드하려면 App Engine용 DataNucleus 플러그인 버전 2.x로 마이그레이션을 참조하세요.

소유 일대일 관계

유형이 관련 클래스의 클래스인 필드를 사용하여 영구 객체 두 개 간에 단방향 일대일 소유 관계를 만듭니다.

다음 예는 ContactInfo 데이터 클래스와 Employee 데이터 클래스를 Employee에서 ContactInfo로의 일대일 관계로 정의합니다.

ContactInfo.java

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

@PersistenceCapable
public class ContactInfo {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String streetAddress;

    // ...
}

Employee.java

import ContactInfo;
// ... imports ...

@PersistenceCapable
public class Employee {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private ContactInfo contactInfo;

    ContactInfo getContactInfo() {
        return contactInfo;
    }
    void setContactInfo(ContactInfo contactInfo) {
        this.contactInfo = contactInfo;
    }

    // ...
}

이들 영구 객체는 데이터 저장소에서 서로 다른 두 종류의 서로 다른 두 개 항목으로 나타납니다. 이들의 관계는 항목 그룹 관계로 나타납니다. 즉, 하위 키는 상위 키를 항목 그룹 상위 항목으로 사용합니다. 앱이 상위 객체의 필드를 사용하여 하위 객체에 액세스하면 JDO 구현은 항목 그룹 상위 쿼리를 수행하여 하위 항목을 가져옵니다.

하위 클래스에는 상위 키 정보(키 또는 문자열로 인코딩된 키 값)를 포함할 수 있는 유형의 키 필드가 있어야 합니다. 키 필드 유형에 대한 자세한 내용은 데이터 만들기: 키를 참조하세요.

두 클래스의 필드를 사용하여 양방향 일대일 관계를 만듭니다. 하위 클래스의 필드에 주석을 달아 필드가 양방향 관계를 나타내도록 선언합니다. 하위 클래스의 필드에는 mappedBy = "..." 인수가 있는 @Persistent 주석이 있어야 합니다. 여기서 값은 상위 클래스의 필드 이름입니다. 한 객체의 필드가 채워지면 다른 객체의 해당 참조 필드가 자동으로 채워집니다.

ContactInfo.java

import Employee;

// ...
    @Persistent(mappedBy = "contactInfo")
    private Employee employee;

처음 액세스할 때 하위 객체는 데이터 저장소에서 로드됩니다. 상위 객체에서 하위 객체에 액세스하지 않으면 하위 객체의 항목은 절대 로드되지 않습니다. 하위 항목을 로드하려면 PersistenceManager를 닫기 전에 '터치'하거나(예: 위 예시와 같이 getContactInfo() 호출) 하위 필드를 기본값 가져오기 그룹에 명시적으로 추가하여 하위 필드가 검색되고 상위 항목과 함께 로드되도록 합니다.

Employee.java

import ContactInfo;

// ...
    @Persistent(defaultFetchGroup = "true")
    private ContactInfo contactInfo;

소유 일대다 관계

한 클래스의 객체와 다른 클래스의 여러 객체 간에 일대다 관계를 만들려면 관련 클래스의 컬렉션을 사용합니다.

Employee.java

import java.util.List;

// ...
    @Persistent
    private List<ContactInfo> contactInfoSets;

일대다 양방향 관계는 일대일과 유사하며 @Persistent(mappedBy = "...") 주석을 사용하는 상위 클래스가 포함됩니다. 여기서 값은 하위 클래스의 필드 이름입니다.

Employee.java

import java.util.List;

// ...
    @Persistent(mappedBy = "employee")
    private List<ContactInfo> contactInfoSets;

ContactInfo.java

import Employee;

// ...
    @Persistent
    private Employee employee;

데이터 클래스 정의: 컬렉션에 나열된 컬렉션 유형이 일대다 관계에 지원됩니다. 단, 배열은 일대다 관계에 지원되지 않습니다.

App Engine은 조인 쿼리를 지원하지 않습니다. 하위 항목의 속성을 사용하여 상위 항목을 쿼리할 수 없습니다. (삽입된 클래스가 상위 항목에 속성을 저장하므로, 삽입된 클래스의 속성을 쿼리할 수 있습니다. 데이터 클래스 정의: 삽입된 클래스를 참조하세요.)

순서가 지정된 컬렉션이 순서를 유지하는 방법

List<...>와 같은 순서가 지정된 컬렉션은 상위 객체 저장 시 객체 순서를 유지합니다. JDO의 경우, 각 객체의 위치를 객체의 속성으로 저장하여 데이터베이스가 이 순서를 유지하도록 해야 합니다. App Engine은 상위 필드 이름 다음에 _INTEGER_IDX가 추가된 속성 이름을 사용하여 해당 항목 속성으로 이를 저장합니다. 위치 속성은 비효율적입니다. 컬렉션에서 요소를 추가, 삭제 또는 이동할 경우, 컬렉션에서 수정된 위치 이후의 모든 항목을 업데이트해야 합니다. 이로 인해 속도가 느려질 수 있으며, 트랜잭션으로 수행하지 않으면 오류가 발생할 수 있습니다.

컬렉션에서 임의의 순서를 유지할 필요는 없지만 순서가 지정된 컬렉션 유형을 사용해야 하는 경우에는 DataNucleus에서 제공되는 JDO의 확장 기능인 주석을 사용하여 요소의 속성을 토대로 순서를 지정할 수 있습니다.

import java.util.List;
import javax.jdo.annotations.Extension;
import javax.jdo.annotations.Order;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    @Order(extensions = @Extension(vendorName="datanucleus",key="list-ordering", value="state asc, city asc"))
    private List<ContactInfo> contactInfoSets = new ArrayList<ContactInfo>();

list-ordering 확장 프로그램을 사용하는 @Order 주석은 컬렉션 요소의 원하는 순서를 JDOQL 순서 지정 절로 지정합니다. 순서는 요소의 속성 값을 사용합니다. 쿼리와 마찬가지로 컬렉션의 모든 요소에 순서 지정 절에 사용되는 속성 값이 있어야 합니다.

컬렉션에 액세스하면 쿼리가 수행됩니다. 필드의 순서 지정 절이 정렬 순서를 두 개 이상 사용하는 경우, 쿼리에는 데이터 저장소 색인이 필요합니다. 자세한 내용은 데이터 저장소 색인 페이지를 참조하세요.

효율성을 위해 가능한 경우, 순서가 지정된 컬렉션 유형의 일대다 관계에는 명시적 순서 지정 절을 사용합니다.

미소유 관계

JDO API는 소유 관계 외에 미소유 관계를 관리할 수 있는 기능을 제공합니다. 이 기능은 사용 중인 App Engine용 DataNucleus 플러그인 버전에 따라 다르게 작동합니다.

  • DataNucleus 플러그인의 버전 1은 자연 구문을 사용하여 미소유 관계를 구현하지 않지만 모델 객체의 인스턴스(또는 인스턴스 컬렉션) 대신에 Key 값을 사용하여 이러한 관계를 계속 관리할 수 있습니다. Key 객체 저장을 두 객체 사이의 임의의 "외부 키"를 모델링하는 것으로 생각할 수 있습니다. 데이터 저장소는 이러한 Key 참조의 무결성을 보장하지 않지만, Key를 사용하면 두 객체 간의 관계를 아주 쉽게 모델링한 후 가져올 수 있습니다.

    단, 이렇게 할 경우 키가 적절한 종류인지 확인해야 합니다. JDO와 컴파일러는 Key 유형을 확인하지 않습니다.
  • DataNucleus 플러그인 버전 2.x는 자연 구문을 사용하여 미소유 관계를 구현합니다.

도움말: 경우에 따라 소유 관계를 미소유 관계인 것처럼 모델링해야 할 수도 있습니다. 소유 관계에 관련된 모든 객체가 자동으로 동일한 항목 그룹에 배치되고 항목 그룹이 초당 1~10 건 쓰기만 지원할 수 있기 때문입니다. 예를 들어 상위 객체가 초당 0.75건의 쓰기를 받고 하위 개체가 초당 0.75건의 쓰기를 받는 경우, 상위 항목과 하위 항목이 모두 고유한 독립 항목 그룹에 존재하도록 이 관계를 미소유로 모델링하는 것이 좋습니다.

미소유 일대일 관계

사람과 음식을 모델링한다고 가정합니다. 이 때 한 사람은 좋아하는 음식을 하나만 가질 수 있지만, 이 음식은 여러 사람이 좋아하는 음식일 수 있으므로 해당 사람에게 속하지 않습니다. 이 섹션은 이를 수행하는 방법을 보여 줍니다.

JDO 2.3

이 예시에서는 PersonKey 유형의 구성원을 부여합니다. 여기서 KeyFood 객체의 고유 식별자입니다. Person.favoriteFood 인스턴스에서 참조하는 Person 인스턴스와 Food 인스턴스가 동일한 항목 그룹에 속해 있지 않은 경우 JDO 구성이 교차 그룹(XG) 트랜잭션을 사용 설정하도록 설정되어 있지 않으면 단일 트랜잭션에 사람과 이 사람의 좋아하는 음식을 업데이트할 수 없습니다.

Person.java

// ... imports ...

@PersistenceCapable
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private Key favoriteFood;

    // ...
}

Food.java

import Person;
// ... imports ...

@PersistenceCapable
public class Food {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    // ...
}

JDO 3.0

이 예시에서는 Person에 좋아하는 음식을 나타내는 키를 제공하는 대신 Food 유형의 비공개 구성원을 만듭니다.

Person.java

// ... imports ...

@PersistenceCapable
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    @Unowned
    private Food favoriteFood;

    // ...
}

Food.java

import Person;
// ... imports ...

@PersistenceCapable
public class Food {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    // ...
}

미소유 일대다 관계

이제 한 사람이 좋아하는 음식을 여러 개 가질 수 있다고 가정합니다. 다시 한 번, 좋아하는 음식 한 개는 여러 사람이 좋아하는 음식일 수 있으므로 해당 사람에게 속하지 않습니다.

JDO 2.3

이 예시에서는 사람이 가장 좋아하는 음식을 나타내기 위해 Persone에게 Set<Food> 유형의 구성원을 제공하는 대신 Set<Key> 유형의 구성원을 제공합니다. 이 집합에는 Food 객체의 고유 식별자가 포함되어 있습니다. Person.favoriteFoods에 포함된 Person 인스턴스와 Food에 인스턴스가 동일한 항목 그룹에 속해 있지 않으면 JDO 구성이 교차 그룹(XG) 트랜잭션을 사용 설정하도록 설정해야 같은 트랜잭션에서 인스턴스를 업데이트할 수 있습니다.

Person.java

// ... imports ...

@PersistenceCapable
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private Set<Key> favoriteFoods;

    // ...
}

JDO 3.0

이 예시에서는 Person에게 Set<Food> 유형의 구성원을 제공합니다. 여기서 집합은 Person이 가장 좋아하는 음식을 나타냅니다.

Person.java

// ... imports ...

@PersistenceCapable
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private Set<Food> favoriteFoods;

    // ...
}

다대다 관계

관계의 양쪽에 키 컬렉션을 유지하여 다대다 관계를 모델링할 수 있습니다. Food가 자신을 좋아하는 사람을 추적할 수 있도록 예시를 조정해 보겠습니다.

Person.java

import java.util.Set;
import com.google.appengine.api.datastore.Key;

// ...
    @Persistent
    private Set<Key> favoriteFoods;

Food.java

import java.util.Set;
import com.google.appengine.api.datastore.Key;

// ...
    @Persistent
    private Set<Key> foodFans;

이 예시에서 Person은 즐겨 찾는 Food 객체를 고유하게 식별하는 Key 값 집합을 유지하고 Food는 자신을 좋아하는 Person 객체를 고유하게 식별하는 Key 값 집합을 유지합니다.

Key 값을 사용하여 다대다 관계를 모델링할 경우 앱에서 관계의 양쪽을 유지해야 합니다.

Album.java

// ...
public void addFavoriteFood(Food food) {
    favoriteFoods.add(food.getKey());
    food.getFoodFans().add(getKey());
}

public void removeFavoriteFood(Food food) {
    favoriteFoods.remove(food.getKey());
    food.getFoodFans().remove(getKey());
}

Person.favoriteFoods에 포함된 Person 인스턴스와 Food 인스턴스가 동일한 항목 그룹에 속해 있지 않으면 JDO 구성이 교차 그룹(XG) 트랜잭션을 사용 설정하도록 설정해야 합니다.

관계, 항목 그룹, 트랜잭션

애플리케이션이 소유 관계가 있는 객체를 Datastore에 저장하면 관계를 통해 연결할 수 있고 저장해야 하는 다른 모든 객체(새로운 객체이거나 마지막으로 로드된 이후 수정된 객체)가 자동으로 저장됩니다. 이는 트랜잭션과 항목 그룹에 중요한 영향을 미칩니다.

EmployeeContactInfo 클래스 간의 단방향 관계를 사용한 다음 예시를 살펴보겠습니다.

    Employee e = new Employee();
    ContactInfo ci = new ContactInfo();
    e.setContactInfo(ci);

    pm.makePersistent(e);

pm.makePersistent() 메서드를 사용하여 새 Employee 객체를 저장하면 새로운 관련 ContactInfo 객체가 자동으로 저장됩니다. 두 객체 모두 새 객체이므로 App Engine은 Employee 항목을 ContactInfo 항목의 상위 항목으로 사용하여 동일한 항목 그룹에 새 항목 두 개를 만듭니다. 마찬가지로 Employee 객체가 이미 저장되어 있고 관련 ContactInfo 객체가 새 객체이면 App Engine은 기존 Employee 항목을 상위 항목으로 사용하여 ContactInfo 항목을 만듭니다.

그러나 이 예시에서 pm.makePersistent() 호출에서는 트랜잭션을 사용하지 않습니다. 명시적 트랜잭션이 없으면 두 항목이 별도의 원자적 동작을 사용하여 생성됩니다. 이 경우, Employee 항목을 성공적으로 만들 수 있지만 ContactInfo 항목을 만들지 못할 수도 있습니다. 두 항목이 모두 성공적으로 생성되거나 생성되지 않도록 하려면 트랜잭션을 사용해야 합니다.

    Employee e = new Employee();
    ContactInfo ci = new ContactInfo();
    e.setContactInfo(ci);

    try {
        Transaction tx = pm.currentTransaction();
        tx.begin();
        pm.makePersistent(e);
        tx.commit();
    } finally {
        if (tx.isActive()) {
            tx.rollback();
        }
    }

관계가 설정되기 전에 두 객체 모두 저장되면 App Engine은 기존 ContactInfo 항목을 Employee 항목의 항목 그룹으로 '이동'할 수 없습니다. 항목 그룹은 항목을 만들 때에만 할당될 수 있기 때문입니다. App Engine은 참조를 통해 관계를 설정할 수 있지만 관련 항목들은 동일한 그룹에 없습니다. 이 경우 JDO 구성이 교차 그룹(XG) 트랜잭션을 사용 설정하도록 설정하면 두 항목을 동일한 트랜잭션에 업데이트하거나 삭제할 수 있습니다. XG 트랜잭션을 사용하지 않는 경우 동일한 트랜잭션에서 서로 다른 그룹의 항목을 업데이트하거나 삭제하려 하면 JDOFatalUserException이 발생합니다.

하위 객체가 수정된 상위 객체를 저장하면 하위 객체에 변경사항이 저장됩니다. 이러한 방식으로 상위 객체가 관련된 모든 하위 객체에 대해 지속성을 유지하도록 하고, 변경사항 저장 시 트랜잭션을 사용하는 것이 좋습니다.

종속된 하위 항목과 연속 삭제

소유 관계는 '종속적'일 수 있습니다. 즉, 하위 항목이 상위 항목 없이 존재할 수 없습니다. 관계가 종속적이고 상위 객체가 삭제되면 모든 하위 객체도 함께 삭제됩니다. 상위 항목의 종속 필드에 새 값을 할당하여 소유 종속 관계를 깨면 이전 하위 항목도 삭제됩니다. 하위 항목을 참조하는 상위 객체 필드의 Persistent 주석에 dependent="true"를 추가하면 소유 일대일 관계를 종속으로 선언할 수 있습니다.

// ...
    @Persistent(dependent = "true")
    private ContactInfo contactInfo;

하위 항목을 참조하는 상위 객체 필드의 @Element(dependent = "true") 주석을 추가하면 소유 일대다 관계를 종속으로 선언할 수 있습니다.

import javax.jdo.annotations.Element;
// ...
    @Persistent
    @Element(dependent = "true")
    private List contactInfos;

객체 만들기 및 업데이트와 마찬가지로 연속 삭제에서 각 삭제 작업을 단일 원자적 동작에서 수행하려면 하나의 트랜잭션에서 삭제를 수행해야 합니다.

참고: JDO 구현은 Datastore가 아닌 종속 하위 객체를 삭제합니다. 하위 수준 API 또는 Google Cloud Console을 사용하여 상위 항목을 삭제하면 관련 하위 객체가 삭제되지 않습니다.

다형성 관계

JDO 사양에는 다형성 관계 지원 기능이 포함되어 있지만, App Engine DO 구현에서는 다형성 관계가 아직 지원되지 않습니다. 이는 향후 제품 출시에서 개선되어야 하는 제한사항입니다. 공통 기본 클래스를 통해 여러 유형의 객체를 참조해야 하는 경우, 미소유 관계의 구현에 사용된 전략과 동일한 전략인 키 참조 저장을 사용하는 것이 좋습니다. 예를 들어 Appetizer, Entree, Dessert 전문 분야가 있는 Recipe 기본 클래스가 있고 선호하는 ChefRecipe를 모델링하려면 다음과 같이 모델링하면 됩니다.

Recipe.java

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.Inheritance;
import javax.jdo.annotations.InheritanceStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable
@Inheritance(strategy = InheritanceStrategy.SUBCLASS_TABLE)
public abstract class Recipe {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private int prepTime;
}

Appetizer.java

// ... imports ...

@PersistenceCapable
public class Appetizer extends Recipe {
// ... appetizer-specific fields
}

Entree.java

// ... imports ...

@PersistenceCapable
public class Entree extends Recipe {
// ... entree-specific fields
}

Dessert.java

// ... imports ...

@PersistenceCapable
public class Dessert extends Recipe {
// ... dessert-specific fields
}

Chef.java

// ... imports ...

@PersistenceCapable
public class Chef {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent(dependent = "true")
    private Recipe favoriteRecipe;
}

하지만 Entree를 인스턴스화하고 Chef.favoriteRecipe에 할당한 경우 Chef 객체를 지속하려고 하면 UnsupportedOperationException이 발생합니다. 객체 Entree의 런타임 유형이 관계 필드 Recipe에 선언된 유형과 일치하지 않기 때문입니다. Chef.favoriteRecipe 유형을 Recipe에서 Key로 바꾸면 해결됩니다.

Chef.java

// ... imports ...

@PersistenceCapable
public class Chef {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private Key favoriteRecipe;
}

Chef.favoriteRecipe는 더 이상 관계 필드가 아니므로 모든 유형의 객체를 참조할 수 있습니다. 하지만 미소유 관계처럼 이 관계를 수동으로 관리해야 합니다.