[Firestore] 3편 - 문서 데이터 쓰기/읽기
https://firebase.google.com/docs/reference/kotlin/com/google/firebase/firestore/package-summary
Firestore api
https://firebase.google.com/docs/firestore/manage-data/add-data?authuser=0
파이어스토어 dependency 추가
//firebase BOM
implementation platform('com.google.firebase:firebase-bom:28.4.1')
//firebase Cloud FireStore
//ktx 라이브러리에는 기본 라이브러리가 포함되어 있기 때문에
//ktx 라이브러리를 추가하면 기본 라이브러리를 추가하지 않아도 된다.
implementation 'com.google.firebase:firebase-firestore'
implementation 'com.google.firebase:firebase-firestore-ktx'
매니페스트 인터넷 권한 추가
FirebaseFirestore (데이터베이스)
1. FirebaseFirestore 인스턴스를 참조
val firestore : FirebaseFirestore = Firebase.firestore // <-파이어스토어 ktx 디펜던시
//또는 FirebaseFirestore.getInstance 로 참조 <-파이어스토어 디펜던시
Firebase 또는 FirebaseFirestore 클래스를 사용
2. 데이터베이스 안에 포함된 컬렉션, 문서 참조
val collection: CollectionReference = mFirestore.collection("컬렉션path")
val document: DocumentReference = mFirestore.document("문서path")
자바의 File class 와 비슷하게
컬렉션의 추상경로는 CollectionReference를 사용하고
문서의 추상경로는 DocumentReference를 사용한다.
그러나 위에서 말했듯이 collection, document 메서드를 사용할 때 path를 넣어야하는데 위에서 설명한 짝수,홀수를 맞추지 않으면 예외가 발생한다.
CollectionReferenece (컬렉션 위치 참조할 수 있는 클래스. 실제 컬렉션 유무상관없이 추상경로에 해당)
extends Query
쿼리 클래스에 컬렉션의 데이터 변화를 감지하는 리스너, SQL을 모방한 where, order, limit 등의 메서드가 존재한다.
밑에서 추가설명
메서드
1. 컬렉션 아래에 데이터 빠르게 write하기
open fun add(data: Any): Task<DocumentReference!>
케이스에 따라서 문서의 id가 필요없을 수도 있다.
컬렉션 바로 하위에 자동id(Firebase지정)문서를 만들고 해당 문서에 data를 write한다.
data: Map 또는 POJO 또는 FiledValue 가능 (밑에서 설명)
리턴되는 Task를 참조하여 Task에 성공,실패,완료 리스너를 달 수 있다.
class TestActivity : AppCompatActivity() {
private lateinit var mFirestore: FirebaseFirestore
private lateinit var mBinding: ActivityTestBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_test)
mFirestore = Firebase.firestore
val user = hashMapOf(
"성" to "홍",
"이름" to "길동",
"출생" to 1900
)
val collectionRef: CollectionReference = mFirestore.collection("users")
val task: Task<DocumentReference> = collectionRef.add(user)
task.addOnSuccessListener { documentReference ->
log("DocumentSnapshot added with ID: ${documentReference.id}")
}.addOnFailureListener { exception ->
log("Error adding document: ${exception.toString()}")
}
}
private fun log(str: String) {
Log.d("MY_APPLICATION", str)
}
}
Firebase는 컬렉션에는 id자동지정이 없지만 , 문서에는 id자동지정을 제공해준다.
데이터 구조에 따라 아래와 같이 배치할 수 있다(콘솔을 통해 조작하였음)
--성에 해당하는 홍을 문서의 이름으로 지정
--성에 해당하는 홍을 문서 이름 대신 데이터의 추가적인 필드로 지정
2. 컬렉션 정보 출력- getId: String , getPath: String , getParent: DocumentReference?
컬렉션의 부모는 컬렉션이 될 수 없다. 그렇기 때문에 getParent 메서드의 리턴은 항상 문서이다.
단 루트 컬렉션의 경우에는 부모가 없어 null을 리턴한다.
class TestActivity : AppCompatActivity() {
private lateinit var mFirestore: FirebaseFirestore
private lateinit var mBinding: ActivityTestBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_test)
mFirestore = Firebase.firestore
val collectionRef: CollectionReference = mFirestore.collection("컬1")
with(collectionRef) {
log("collectionRef")
log("id: $id")
log("path: $path")
log("부모 Document: ${parent?.id}") //DocumentReference#getId
log("..........................")
}
val collectionRef2: CollectionReference = mFirestore.collection("컬1/문1/컬2")
with(collectionRef2) {
log("collectionRef2")
log("id: $id")
log("path: $path")
log("부모 Document: ${parent?.id}")
}
}
private fun log(str: String) {
Log.d("MY_APPLICATION", str)
}
}
3. 해당 컬렉션 하위의 문서 참조 생성
컬렉션 바로 하위의 Firebase가 생성해준 자동id 문서 참조 생성
open fun document(): DocumentReference
File api처럼 인스턴스는 가상 공간의 위치(추상경로 abstract path)만 참조하는 것이지 문서를 생성하는 것이 아니다.
컬렉션 모든 하위들에 포함된 문서 참조 생성
open fun document(documentPath: String): DocumentReference
컬렉션1#document("/문서1")
컬렉션1#document("/문서1/컬렉션2/문서2")
DocumentReference( 문서 위치 참조 클래스. 실제 문서 유무 상관없이 문서의 추상경로에 해당)
도큐먼트 정보를 출력하는 메서드 getId, getParent: CollectionRefrence, getPath
하위 컬렉션 참조 메서드 collection
위의 두개는 컬렉션과 같거나 비슷하므로 생략
+ 데이터는 문서에만 write 가능하므로 DocumentReference에 데이터 read, update, set(덮어쓰기), delete 메서드
+ 리스너 등록( 데이터 변경시 호출) addSnapshotListener
+ hashCode, equals (코드상 변수 비교)
+ getFirestore (해당 문서의 Firestore데이터베이스. 왜 필요한 것일까?)
리턴 타입 정리
get 만 Task<DocumentSnapshot!> 리턴
나머지 delete, set, update 모두 Task<Void!> 리턴
- set 메서드
-- data: Any
문서가 존재하지 않으면 문서 생성후 쓰기 , 문서가 존재하면 덮어쓰기
예제1
예제2
Map을 통해서만 데이터 작업을 하기에는 불편한점이 존재하기 때문에 Custom 객체를 통해 데이터 작업을 할 수 있다.
데이터들이 혼합 유형일 때 각 nested 혼합 데이터들을 각 클래스로 선언하여 재활용하여 사용한다.
-- data: Any, options: SetOptions
해당 셋 옵션으로 데이터를 write
SetOptions class
SetOptions merge인 경우 문서 교체가 아닌 필드만 추가시켜 병합
set 전
옵션을 통해 set메서드 후
- delete()
해당 문서 삭제( 하위 컬렉션들 , 문서의 데이터 모두 삭제됨)
- update 메서드들 공통
일부 필드만 업데이트하는 메서드
문서가 존재하지 않으면 업데이트 하지 않고, 문서가 존재할 때만 수행됨
-- data : MutableMap<String!, Any!>
MutableMap의 키들에 해당하는 field들의 value 업데이트
업데이트 전
val document = mFirestore.document("testCollection/testDocument")
val mutableMap: MutableMap<String, Any> = mutableMapOf("field1" to "교체1", "field2" to "교체2")
document.update(mutableMap)
업데이트 후
중첩된 필드의 업데이트가 필요할 경우 ( key에 필드.업데이트될중첩필드 )
업데이트 전
업데이트 후
-- field: String, value: Any?, vararg moreFieldsAndValues: Any!
-- fieldPath: FieldPath, value: Any?, vararg moreFieldsAndValues: Any!
4. get
get()메서드는 서버로부터 기다려서 최신 데이터를 가져온다.
(인터넷이 오프라인인 경우 캐시된 것을 가져오고, 서버응답이 없으면 실패할 수 도있다)
get(source: Source)는 데이터를 어떤 방식으로 가져올 것인지 정한다.
com.google.firebase.firestore.Source
DocumentReference, Query 클래스의 Source인자가 달린 get메서드에 적용할 수 있다.
https://firebase.google.com/docs/reference/android/com/google/firebase/firestore/Source
서버는 문서 데이터 요청을 문제없이 성공적으로 받아들이면 디바이스에 데이터를 캐싱한다.
Source.CACHE
서버를 완전히 무시하고 디바이스의 캐시에서 가져온다.
문서 데이터를 처음 가져올 때 get()메서드를 사용하지 않은 상태에서 get(Source.CACHE)메서드를 사용하면 디바이스에 캐싱된 값이 없기 때문에 에러가 발생한다. 성공 리스너 메서드 대신 실패 리스너가 호출된다.
Source.DEFAULT
서버에서 문서의 데이터를 가져오지만
서버이용 불가능한 경우 대비책(fall back)으로 디바이스에 캐싱된 데이터를 사용한다.
Source.SERVER
디폴트의 동작을 피하기 위해( 최신 데이터 보장을 위해) 사용한다. 무조건 서버에서만 가져온다.
서버 접근 못할시 에러발생. 서버가 요청을 성공하면 디바이스에 데이터 캐싱은 그대로 진행된다.
FieldValue (문서에 데이터를 write하는 작업에 data대신 사용가능)
앞서 문서에 데이터를 쓰거나, 업데이트 하는 경우 메서드에 map이나 커스텀 객체을 넣어주었다.
메서드에 FieldValue클래스 인스턴스를 넣어 쉽게 필드의 값을 쓸 수 있다.
배열값 쓰기
가변인자에 해당하는 값들을 element들로 취급하여 필드에 set하거나, update한다.
만약 필드의 값이 기존에 배열이고, 기존 필드의 배열 요소중 가변인자와 동일한 값이 있을 경우에는 추가시키지 않고 변경사항이 없다. 가변인자들에 해당하는 값이 필드의 배열에 존재하지 않을 때만 배열의 마지막 부분에 추가된다.
만약 필드의 값이 배열이 아니면, 배열로 덮어쓴다.
필드 : {1,2,3,4}
update(필드, FieldValue.arrayUnion(4)) -> 필드의 배열에 4가 존재하므로 업데이트 사항 없음
기존 문서 데이터( 필드 값 스트링)
코드 실행 후
코드 실행 후 배열의 마지막 element에 추가됨
같은 코드를 한번 더 실행하면 필드의 배열에 이미 존재하는 값이라 변경사항이 없다.
메서드의 가변인자들이 필드의 배열에 존재하면 제거하고 없으면 아무일도 일어나지 않는다.
필드가 배열이 아닌 상태라면 빈 배열로 덮어쓴다.
기존 문서 필드의 상태(배열)
기존 문서 필드의 상태(배열 아님)
필드 값 삭제
https://firebase.google.com/docs/firestore/manage-data/delete-data?authuser=0
update메서드시에만(set() x) 필드를 삭제하기 위해 사용
필드 삭제됨
필드 값 증가/감소
set, update 모두 사용가능
필드가 존재하지 않거나, 필드가 numeric(실수,정수형)이 아닌경우 제공된 값으로 write, overwrite한다.
1. 정수형 증가/감소
만약 필드가 정수형일때 메서드를 사용하여 양의 오버플로우가 나게되면 +에서 -가되는 것이아니라 Long.MAX_VALUE로 갚을 쓴다.( 음의 오버플로우도 마찬가지 Long.MIN_VALUE로 )
필드 존재하지 않을 때 인자의 값으로 값을 썼다.
오버플로우 시 Long의 최대값으로
만약 필드가 실수형이면, 인자가 실수형으로 변환되어 갚을 쓴다.
기존
2. 실수형 증가/감소
필드가 정수/실수형인 경우모두 Double로 변환되어 계산된다.
이외의 모든 케이스 (필드가 다른 타입인 경우, 필드가 없는 경우)는 해당 값으로 필드값을 write한다.
이 동작은 카운터를 구현하는데 유용합니다. 그러나 1초에 한번만 문서를 업데이트 할 수 있습니다.
https://firebase.google.com/docs/firestore/query-data/get-data?authuser=0
문서의 데이터 읽어오기
DocumentReference#get 메서드를 통해 Task<DocumentSnapshot!>객체를 리턴받습니다.
이 Task객체는 성공,실패, 완료(태스크 작업이 끝났을때) 리스너를 등록할 수 있습니다.
성공 리스너 구현에서 DocumentSnapshot을 전달받아 데이터를 뽑아 낼 수 있습니다.
문서 샘플 데이터
문서는 존재하나 데이터가 비어있는 경우 DataSnapshot은 아무것도 가지고 있지 않습니다.
또한 오프라인일때는 실패 리스너 메서드가 호출됩니다.
mFirestore = FirebaseFirestore.getInstance()
mFirestore.document("collection/document").get()
.addOnSuccessListener {
documentSnapshot ->
log("Task 성공")
try {
with(documentSnapshot) {
val numberList: List<Int>? = get("fieldArray") as? List<Int>
if (numberList != null) {
log("int 리스트: $numberList")
} else {
log("field value is not list(Firestore array)")
}
log("string: ${getString("fieldString")}")
log("boolean: ${getBoolean("fieldBoolean")}")
log("double: ${getDouble("fieldDouble")}")
log("boolean: ${getLong("fieldLong")}")
log("document reference: ${getDocumentReference("fieldDocuRef")!!.path}")
val map: Map<String, String>? = get("fieldMap") as? Map<String, String>
if (map != null) {
log("map: $map")
} else {
log("field value is not map")
}
}
} catch (e: Exception) {
log("Exception in success method: $e")
}
}.addOnFailureListener {
e ->
log("Task 실패")
log("문서가 존재하지 x 또는 기기가 오프라인 ") //일반 get()메서드를 사용할 시에 해당
if (e is FirebaseFirestoreException) {
log("${e.code}")
}
}.addOnCompleteListener {
log("Task 의 작업이 완료될때 호출")
}.addOnCanceledListener {
log("Task 가 취소될 때 호출")
}
문서의 데이터 전체를 JSON처럼 대응되는 Map으로 가져오려면
상위 레벨의 필드부터 추출해나갈 수 있다.
커스텀 클래스의 객체로 데이터 read
위에서 설명했던 커스텀 클래스 객체로 데이터 write에는 구조에 맞는 클래스를 선언하고 객체를 넣어 데이터를 write했다면, 반대로 toObject메서드에 클래스만 지정해주면 문서 데이터가 담긴 객체를 받을 수 있다.