세 가지 방법으로 Cloud Firestore에 저장된 데이터를 검색할 수 있다. 문서, 문서 컬렉션 또는 쿼리 결과에 다음 메서드 중 하나를 사용할 수 있다.
읽을 때도 마찬가지로 인스턴스를 초기화한다.
db = FirebaseFirestore.instance;
우선 도시에 관한 데이터를 작성하고 다양한 방식의 예시를 작성하겠다.
final cities = db.collection("cities");
final data1 = <String, dynamic>{
"name": "San Francisco",
"state": "CA",
"country": "USA",
"capital": false,
"population": 860000,
"regions": ["west_coast", "norcal"]
};
cities.doc("SF").set(data1);
final data2 = <String, dynamic>{
"name": "Los Angeles",
"state": "CA",
"country": "USA",
"capital": false,
"population": 3900000,
"regions": ["west_coast", "socal"],
};
cities.doc("LA").set(data2);
final data3 = <String, dynamic>{
"name": "Washington D.C.",
"state": null,
"country": "USA",
"capital": true,
"population": 680000,
"regions": ["east_coast"]
};
cities.doc("DC").set(data3);
final data4 = <String, dynamic>{
"name": "Tokyo",
"state": null,
"country": "Japan",
"capital": true,
"population": 9000000,
"regions": ["kanto", "honshu"]
};
cities.doc("TOK").set(data4);
final data5 = <String, dynamic>{
"name": "Beijing",
"state": null,
"country": "China",
"capital": true,
"population": 21500000,
"regions": ["jingjinji", "hebei"],
};
cities.doc("BJ").set(data5);
다음 예시에서는 get()을 사용하여 단일 문서의 내용을 검색하는 방법을 보여준다.
final docRef = db.collection("cities").doc("SF");
docRef.get().then(
(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
// ...
},
onError: (e) => print("Error getting document: $e"),
);
이전 예시에서는 문서의 내용을 맵으로 가져왔지만, 일부 언어는 커스텀 객체 유형을 사용하는 것이 더 편리할 수 있다. 데이터 추가 가이드에서 각 도시를 정의하는 데 필요한 City 클래스를 정의했다.
다음과 같이 문서를 City 객체로 되돌릴 수 있다.
커스텀 객체를 사용하려면 클래스에 Firestore 데이터 변환 함수를 정의해야 한다. 예를 들면 다음과 같다.
class City {
final String? name;
final String? state;
final String? country;
final bool? capital;
final int? population;
final List<String>? regions;
City({
this.name,
this.state,
this.country,
this.capital,
this.population,
this.regions,
});
factory City.fromFirestore(
DocumentSnapshot<Map<String, dynamic>> snapshot,
SnapshotOptions? options,
) {
final data = snapshot.data();
return City(
name: data?['name'],
state: data?['state'],
country: data?['country'],
capital: data?['capital'],
population: data?['population'],
regions:
data?['regions'] is Iterable ? List.from(data?['regions']) : null,
);
}
Map<String, dynamic> toFirestore() {
return {
if (name != null) "name": name,
if (state != null) "state": state,
if (country != null) "country": country,
if (capital != null) "capital": capital,
if (population != null) "population": population,
if (regions != null) "regions": regions,
};
}
}
그런 다음 데이터 변환 함수를 사용하여 문서 참조를 만든다. 이 참조를 사용하여 수행하는 읽기 작업은 커스텀 클래스의 인스턴스를 반환한다.
final ref = db.collection("cities").doc("LA").withConverter(
fromFirestore: City.fromFirestore,
toFirestore: (City city, _) => city.toFirestore(),
);
final docSnap = await ref.get();
final city = docSnap.data(); // Convert to City object
if (city != null) {
print(city);
} else {
print("No such document.");
}
컬렉션의 문서를 쿼리하여 하나의 요청으로 여러 문서를 검색할 수도 있다. 예를 들어 where()를 사용하여 특정 조건을 충족하는 모든 문서를 쿼리하고 get()을 사용하여 결과를 가져올 수 있습니다.
db.collection("cities").where("capital", isEqualTo: true).get().then(
(querySnapshot) {
print("Successfully completed");
for (var docSnapshot in querySnapshot.docs) {
print('${docSnapshot.id} => ${docSnapshot.data()}');
}
},
onError: (e) => print("Error completing: $e"),
);
where()문을 삭제해서 모든 문서를 검색할 수도 있다.
하위 컬렉션에서 모든 문서를 검색하려면 해당 하위 컬렉션의 전체 경로가 포함된 참조를 만든다.
db.collection("cities").doc("SF").collection("landmarks").get().then(
(querySnapshot) {
print("Successfully completed");
for (var docSnapshot in querySnapshot.docs) {
print('${docSnapshot.id} => ${docSnapshot.data()}');
}
},
onError: (e) => print("Error completing: $e"),
);
onSnapshot() 메서드로 문서를 리슨할 수 있다. 사용자가 제공하는 콜백이 최초로 호출될 때 단일 문서의 현재 콘텐츠로 문서 스냅샷이 즉시 생성된다. 그런 다음 콘텐츠가 변경될 때마다 콜백이 호출되어 문서 스냅샷을 업데이트
final docRef = db.collection("cities").doc("SF");
docRef.snapshots().listen(
(event) => print("current data: ${event.data()}"),
onError: (error) => print("Listen failed: $error"),
);
Firestore 문서 또는 컬렉션의 콘텐츠 변경사항에 따라 UI가 반응하도록 만들고자 하는 경우, Firestore 스냅샷 스트림을 사용하는 StreamBuilder 위젯을 사용.
class UserInformation extends StatefulWidget {
_UserInformationState createState() => _UserInformationState();
}
class _UserInformationState extends State<UserInformation> {
final Stream<QuerySnapshot> _usersStream =
FirebaseFirestore.instance.collection('users').snapshots();
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _usersStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return const Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Loading");
}
return ListView(
children: snapshot.data!.docs
.map((DocumentSnapshot document) {
Map<String, dynamic> data =
document.data()! as Map<String, dynamic>;
return ListTile(
title: Text(data['full_name']),
subtitle: Text(data['company']),
);
})
.toList()
.cast(),
);
},
);
}
}
앱에서 로컬로 쓰면 즉시 스냅샷 리스너가 호출된다. 이는 '지연 시간 보정'이라는 중요한 기능 때문. 쓰기를 수행하면 데이터가 백엔드로 전송되기 전에 리스너에 새 데이터가 통보된다.
검색된 문서의 metadata.hasPendingWrites 속성은 문서에 아직 백엔드에 쓰기 처리되지 않은 로컬 변경사항이 있는지 여부를 나타낸다. 이 속성을 사용하여 스냅샷 리스너가 수신한 이벤트의 소스를 확인할 수 있다.
final docRef = db.collection("cities").doc("SF");
docRef.snapshots().listen(
(event) {
final source = (event.metadata.hasPendingWrites) ? "Local" : "Server";
print("$source data: ${event.data()}");
},
onError: (error) => print("Listen failed: $error"),
);
문서와 마찬가지로 get() 대신 onSnapshot()을 사용하여 쿼리 결과를 리슨할 수 있습니다. 이렇게 하면 쿼리 스냅샷이 만들어진다. 예를 들어 주가 CA인 문서를 리슨하는 방법은 다음과 같다.
db
.collection("cities")
.where("state", isEqualTo: "CA")
.snapshots()
.listen((event) {
final cities = [];
for (var doc in event.docs) {
cities.add(doc.data()["name"]);
}
print("cities in CA: ${cities.join(", ")}");
});
단순히 전체 쿼리 스냅샷을 사용하는 대신 쿼리 스냅샷 간에 실제로 변경된 쿼리 결과를 확인하는 것이 유용한 경우가 많다. 일례로 개별 문서가 추가, 삭제, 수정될 때 캐시를 유지해야 하는 경우가 있다.
db
.collection("cities")
.where("state", isEqualTo: "CA")
.snapshots()
.listen((event) {
for (var change in event.docChanges) {
switch (change.type) {
case DocumentChangeType.added:
print("New City: ${change.doc.data()}");
break;
case DocumentChangeType.modified:
print("Modified City: ${change.doc.data()}");
break;
case DocumentChangeType.removed:
print("Removed City: ${change.doc.data()}");
break;
}
}
});
초기 상태는 서버에서 직접 가져오거나 로컬 캐시에서 가져올 수 있다. 로컬 캐시에 사용 가능한 상태가 있으면 쿼리 스냅샷에 우선 캐시된 데이터가 채워진 후, 클라이언트에서 서버의 상태를 따라잡았을 때 서버의 데이터로 업데이트된다.
더 이상 데이터를 리슨할 필요가 없으면 이벤트 콜백이 호출되지 않도록 리스너를 분리해야 한다. 이렇게 하면 클라이언트에서 업데이트 수신을 위한 대역폭 사용을 중지한다. 예를 들면 다음과 같다.
final collection = db.collection("cities");
final listener = collection.snapshots().listen((event) {
// ...
});
listener.cancel();
보안 권한이 부족하거나 잘못된 쿼리를 리슨하는 등의 경우에 리슨이 실패할 수 있습니다. 이러한 오류를 처리하려면 스냅샷 리스너를 연결할 때 오류 콜백을 제공한다. 오류가 발생하면 리스너는 이벤트를 더 이상 수신하지 않으므로 리스너를 분리할 필요는 없다.
final docRef = db.collection("cities");
docRef.snapshots().listen(
(event) => print("listener attached"),
onError: (error) => print("Listen failed: $error"),
);
쿼리의 종류가 너무 많아서, 간단히 쿼리 연산자만 블로그에 작성하고, 링크로 확인하는게 더 나을듯하다.
where() 메서드는 필터링할 필드, 비교 연산, 값의 3가지 매개변수를 사용한다. Cloud Firestore는 다음과 같은 비교 연산자를 지원한다.
array-containsarray-contains-anyinnot-in사용 예시는 아래 Reference의 공식문서 - 단순 쿼리 및 복합 쿼리 실행을 참조하자 ㅠㅠ
Cloud Firestore는 컬렉션에서 검색할 문서를 지정하는 강력한 쿼리 기능을 제공한다. 데이터 가져오기에 설명된 대로 이러한 쿼리를 get() 또는 addSnapshotListener()와 함께 사용할 수도 있다.
기본적으로 쿼리는 쿼리 조건에 맞는 모든 문서를 문서 ID에 따라 오름차순으로 검색한다. orderBy()를 사용하여 데이터의 정렬 순서를 지정하고 limit()를 사용하여 검색된 문서 수를 제한할 수 있다. limit()를 지정하는 경우 값은 0보다 크거나 같아야 한다.
이 부분도 문서를 바로 필요할 때 문서를 바로 참고하는 것이 좋을 것 같다. 공식문서 - 데이터 정렬 및 제한 참고.