콘솔로 이동

배열, 목록, 집합 다루기

앱에서 배열과 같은 데이터 구조를 문서에 저장해야 하는 경우가 많습니다. 예를 들어 사용자 문서는 사용자가 가장 자주 연락하는 친구 5명의 목록을 포함할 수 있고, 블로그 글 문서는 관련 카테고리 세트를 포함할 수 있습니다.

Cloud Firestore는 배열을 저장할 수 있지만 배열의 멤버를 쿼리하거나 단일 배열 요소를 업데이트하는 기능은 지원하지 않습니다. 그러나 Cloud Firestore의 다른 기능을 활용하여 이러한 종류의 데이터를 모델링할 수 있습니다.

계속하기 전에 Cloud Firestore 데이터 모델을 꼭 읽어보시기 바랍니다.

솔루션 : 값의 맵

Cloud Firestore에서 배열과 같은 데이터를 모델링하는 한 가지 방법은 키와 부울 값을 대응시키는 맵으로 데이터를 표현하는 것입니다. 예를 들어 블로그 앱에서 각 글에 여러 카테고리가 태그로 지정되어 있다고 가정해 보겠습니다. 배열을 사용하는 경우 블로그 글 문서는 다음과 같은 구조입니다.

// Sample document in the 'posts' collection.
{
    title: "My great post",
    categories: [
        "technology",
        "opinion",
        "cats"
    ]
}

Swift

struct PostArray {

    let title: String
    let categories: [String]

    init(title: String, categories: [String]) {
        self.title = title
        self.categories = categories
    }

}

let myArrayPost = PostArray(title: "My great post",
                            categories: ["technology", "opinion", "cats"])

Android

public class ArrayPost {
    String title;
    String[] categories;

    public ArrayPost(String title, String[] categories) {
        this.title = title;
        this.categories = categories;
    }
}
ArrayPost myArrayPost = new ArrayPost("My great post", new String[]{
        "technology", "opinion", "cats"
});

'cats' 범주에 속하는 모든 글을 쿼리하려면 어떻게 해야 할까요? 위와 같은 데이터 구조에서는 이 쿼리를 수행할 방법이 없습니다.

다음과 같이 각 카테고리가 키고 모든 값이 true인 맵을 사용한 데이터 구조가 대안이 될 수 있습니다.

// Sample document in the 'posts' collection
{
    title: "My great post",
    categories: {
        "technology": true,
        "opinion": true,
        "cats": true
    }
}

Swift

struct PostDict {

    let title: String
    let categories: [String: Bool]

    init(title: String, categories: [String: Bool]) {
        self.title = title
        self.categories = categories
    }

}

let post = PostDict(title: "My great post", categories: [
    "technology": true,
    "opinion": true,
    "cats": true
])

Android

public class MapPost {
    String title;
    Map<String,Boolean> categories;

    public MapPost(String title, Map<String,Boolean> categories) {
        this.title = title;
        this.categories = categories;
    }
}
Map<String, Boolean> categories = new HashMap<>();
categories.put("technology", true);
categories.put("opinion", true);
categories.put("cats", true);
MapPost myMapPost = new MapPost("My great post", categories);

이제 단일 카테고리에 속하는 모든 블로그 글을 쉽게 쿼리할 수 있습니다.

 // Find all documents in the 'posts' collection that are
// in the 'cats' category.
db.collection('posts')
    .where('categories.cats', '==', true)
    .get()
    .then(() => {
        // ...
    });)

Swift

db.collection("posts")
    .whereField("categories.cats", isEqualTo: true)
    .getDocuments() { (querySnapshot, err) in

        // ...

}

Android

db.collection("posts")
        .whereEqualTo("categories.cats", true)
        .get()
        .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
            // ...
        });

이 기법에서는 Cloud Firestore가 중첩된 맵의 필드를 포함하여 모든 문서 필드에 대한 기본 색인을 만든다는 점을 활용합니다.

게시 날짜를 기준으로 정렬된 단일 카테고리의 모든 블로그 글과 같이 복잡한 쿼리는 어떻게 처리해야 할까요? 다음과 같은 쿼리를 시도해 볼 수 있습니다.

잘못된 쿼리

 db.collection('posts')
    .where('categories.cats', '==', true)
    .orderBy('timestamp');)

Swift

db.collection("posts")
    .whereField("categories.cats", isEqualTo: true)
    .order(by: "timestamp")

Android

db.collection("posts")
        .whereEqualTo("categories.cats", true)
        .orderBy("timestamp");

범위 필터가 있는 여러 필드에 대한 쿼리에는 복합 색인이 필요합니다. 그러나 위 방법으로는 이와 같은 작업이 불가능합니다. 색인이 특정 필드 경로에 정의되어야 합니다. 가능한 각 필드 경로(categories.cats 또는 categories.opinion)에 색인을 만들어야 합니다. 사용자가 생성한 카테고리인 경우에는 사전에 색인을 만들 수 없습니다.

쿼리에 대한 모든 정보를 맵 값에 인코딩하면 이 같은 제한을 해결할 수 있습니다.

// The value of each entry in 'categories' is a unix timestamp
{
  title: "My great post",
  categories: {
    technology: 1502144665,
    opinion: 1502144665,
    cats: 1502144665
  }
}

Swift

struct PostDictAdvanced {

    let title: String
    let categories: [String: UInt64]

    init(title: String, categories: [String: UInt64]) {
        self.title = title
        self.categories = categories
    }

}

let dictPost = PostDictAdvanced(title: "My great post", categories: [
    "technology": 1502144665,
    "opinion": 1502144665,
    "cats": 1502144665
])

Android

public class MapPostAdvanced {
    String title;
    Map<String,Long> categories;

    public MapPostAdvanced(String title, Map<String,Long> categories) {
        this.title = title;
        this.categories = categories;
    }
}
Map<String, Long> categories = new HashMap<>();
categories.put("technology", 1502144665L);
categories.put("opinion", 1502144665L);
categories.put("cats", 1502144665L);
MapPostAdvanced myMapPostAdvanced = new MapPostAdvanced("My great post", categories);

이제 복합 색인 없이도 원하는 쿼리를 단일 필드의 조건 집합으로 표현할 수 있습니다.

올바른 쿼리

 db.collection('posts')
    .where('categories.cats', '>', 0)
    .orderBy('categories.cats');)

Swift

db.collection("posts")
    .whereField("categories.cats", isGreaterThan: 0)
    .order(by: "categories.cats")

Android

db.collection("posts")
        .whereGreaterThan("categories.cats", 0)
        .orderBy("categories.cats");

부울 맵 사용 시와 비교할 때 이 방법에서는 맵 값이 서로 동기화되어야 하므로 데이터를 업데이트하기가 더 어렵습니다.

제한사항

위 솔루션은 Cloud Firestore에서 배열과 같은 구조를 시뮬레이션하는 좋은 방법이지만 다음과 같은 제한사항에 유의해야 합니다.

  • 색인 생성 제한 - Cloud Firestore 기본 색인을 사용하는 경우 단일 문서의 속성이 20,000개로 제한됩니다. 배열과 같은 데이터 구조의 구성 항목이 수만 개 이상으로 늘어날 경우 이 한도에 도달할 수 있습니다.
  • 문서 크기 - Cloud Firestore는 작은 문서에 최적화되어 있으며 문서의 크기를 1MB로 제한합니다. 배열이 임의로 확장될 수 있는 경우 확장 성능이 더 우수한 하위 컬렉션을 사용하는 것이 좋습니다.