2024. 5. 7. 17:55ㆍIT/Android
<정리>
FirebaseDatabase.getInstance() 메서드를 사용하여 데이터 베이스 객체를 생성하고, 생성된 데이터 베이스 객체의 reference 프로퍼티를 사용하여 루트 노드를 참조한다.
private val database: FirebaseDatabase = FirebaseDatabase.getInstance()
private val reference: DatabaseReference = database.reference
child() 메서드를 사용하여 자식 노드를 참조할 수 있는데, 데이터 베이스에 해당 노드가 존재하지 않아도 참조가 생성된다. 단, 생성된 참조를 사용하여 값을 삽입하지 않으면 데이터 베이스에는 반영되지 않는다. 다시 말해, 데이터 베이스에 "user"라는 자식 노드가 존재하지 않아도 코드 상에서는 참조 생성이 가능하다.
private val reference2 = reference.child("user")
생성된 참조와 setValue() 메서드를 사용하여 데이터를 삽입하는데, 인자로 전달되는 사용자 타입 객체는 키-값 쌍의 JSON 타입으로 변경되어 데이터 베이스에 저장된다.
data class Person(
val sex: String,
val name: String,
val phoneNum: String,
val age: Int
)
val person = Person("male", "Kim", "010-5353-3434", 25)
private fun insertKotlinObject() {
reference2.setValue(person)
}
데이터를 포함하고 있는 기존의 참조에 또 다른 데이터를 저장하면, 데이터가 추가되는 것이 아니라 기존의 데이터는 사라지고 새로운 데이터가 덮어 씌워진다. person이 저장된 "user"에 List<Person> 타입의 people을 저장했더니 데이터 베이스에 "Kim"은 사라지고 "James, Catherine, Michel"만 존재하는 것을 확인할 수 있다. 참고로 특정 참조 노드에 null 값을 삽입하면 해당 노드는 삭제된다.
val people = listOf<Person>(
Person(
name = "James",
age = 18,
phoneNum = "010-1234-5678",
sex = "male"
),
Person(
name = "Catherine",
age = 20,
phoneNum = "010-1111-2222",
sex = "female"
),
Person(
name = "Michel",
age = 21,
phoneNum = "010-2222-3333",
sex = "male"
)
)
private fun insertKotlinObjectList() {
reference2.setValue(people)
}
데이터 베이스에 저장된 데이터는 특정 참조에 리스너를 추가하여 "스냅샷"을 통해 읽을 수 있는데, 여기서 "스냅샷"은 리스너가 호출될 때 생성되는 일시적 데이터 복사본을 의미한다. 일반적으로 RealTime Database 서버로부터 데이터를 요청하면 스냅샷이 생성되고, 이 생성된 스냅샷은 로컬 캐시에 저장된다. 로컬 캐시에 저장된 스냅샷은 아래의 각각 다른 특징을 갖는 3가지 리스너와 함께 작용한다.
1. addValueEventListener : 초기 데이터 베이스의 상태를 가져오기 위해 한 번 호출되고, 해당 참조 노드를 포함하여 연결된 모든 자식 노드 중 하나라도 값이 변경될때마다 호출되는데 이때 참조 노드와 연결된 모든 자식 노드의 데이터를 다시 읽어온다.
2. addChildEventListener : 마찬가지로 초기 데이터 베이스의 상태를 가져오기 위해 한 번 호출되고, 해당 참조 노드를 포함하여 연결된 모든 자식 노드 중 하나라도 값이 변경될때마다 호출되지만 자식 노드의 변경에 특화되어 각 상황에 맞는 이벤트가 발생하고 변경된 노드의 데이터만 다시 읽어온다.
3. addListenerForSingleValueEvent : 참조 노드와 연결된 모든 자식 노드의 데이터를 단 한번만 읽어오고, 한 번 데이터를 읽은 후에는 해당 참조 노드의 데이터가 변경되어도 호출되지 않는다. 데이터의 변경을 감지하고 호출되는 것이 아니라 현재 상태의 데이터를 목적으로 호출하기 때문에, 이전에 데이터의 호출이 있었다면 로컬 캐시의 스냅샷을 가져와 효율을 높이고 그렇지 않으면 서버로부터 데이터를 가져온다. 다만, 호출 빈도가 높으면 불필요한 대역폭을 증가시킬 수 있어 주의가 필요하다.
+ get : addListenerForSingleValueEvent와 비슷하지만 동기적으로 실행된다.
reference2.addListenerForSingleValueEvent(object : ValueEventListener{
override fun onDataChange(snapshot: DataSnapshot) {
//데이터가 변경되면 호출.
}
override fun onCancelled(error: DatabaseError) {
//네트워크, 권한 등으로 인해 데이터를 읽을 수 없으면 호출.
}
})
reference2.addValueEventListener(object: ValueEventListener{
override fun onDataChange(snapshot: DataSnapshot) {
//데이터가 변경되면 호출.
}
override fun onCancelled(error: DatabaseError) {
//네트워크, 권한 등으로 인해 데이터를 읽을 수 없으면 호출.
}
})
reference2.addChildEventListener(object: ChildEventListener {
override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
//자식 노드가 추가되면 호출.
}
override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
//자식 노드가 변경되면 호출.
}
override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
//자식 노드가 이동되면 호출.
}
override fun onChildRemoved(snapshot: DataSnapshot) {
//자식 노드가 제거되면 호출.
}
override fun onCancelled(error: DatabaseError) {
//네트워크, 권한 등으로 인해 데이터를 읽을 수 없으면 호출.
}
})
database.child("user").get().addOnSuccessListener {
//데이터를 읽으면 호출.
}.addOnFailureListener{
//데이터를 읽을 수 없으면 호출.
}
데이터 베이스로부터 읽은 데이터는 스냅샷(DataSnapshot)의 형태로 받아오는데, 이 스냅샷에 대해 getValue() 메서드를 호출하면 데이터를 Any? 혹은 사용자 정의 타입으로 얻을 수 있다. 만약 사용자 정의 타입으로 받는다면 해당 클래스는 기본 생성자를 포함해야 하고, 각 프로퍼티에 public getter를 포함해야 한다. 여기서 코틀린 사용자는 data 클래스의 프로퍼티에 기본 값을 설정해야 자바 클래스로 변환될 때 public getter가 포함되어 예외가 발생하지 않는다.
data class Person(
val sex: String = "",
val name: String = "",
val phoneNum: String = "",
val age: Int = 0
)
private fun pullJSONObject() {
reference2.addListenerForSingleValueEvent(object : ValueEventListener{
override fun onDataChange(snapshot: DataSnapshot) {
val person = snapshot.getValue<Person>()
}
//...
})
}
만약 data 클래스에 기본 값을 지정하지 않으면 public getter가 포함되지 않아 아래의 예외를 얻을 수 있다.
java.util.NoSuchElementException: No value present
updateChildren() 메서드를 사용하여 복잡한 하위 노드 구조에서 특정 다수 노드의 필드를 선택하여 값을 변경할 수 있다. 예를 들어, person/0/sex와 user/person/sex의 값을 male에서 female로 변경한다고 하자.
아래의 코드에 주석을 달아서 설명.
private fun updateJSONObject() {
val updates = hashMapOf<String, Any>( //변경 내용을 HashMap으로 생성한다.
"/person/0/sex" to "female", //person 아래 0 아래 sex 필드를 female로 변경.
"user/person/sex" to "female" //user 아래 person 아래 sex 필드를 female로 변경.
)
database.reference.updateChildren(updates) //변경을 적용.
}
따라서 아래의 변경이 적용된다.
*참고로 updateChildren()은 완료, 실패 시 실행되는 콜백을 지원하고, 원자적으로 실행되어 부분만 변경되는 경우는 없다.
updateChildren() 메서드를 사용하여 복잡한 하위 노드 구조에서 특정 다수 노드를 선택하여 하나의 데이터를 동시에 삽입할 수 있다. 예를 들어, 아래의 구조에서 "Emma"라는 사람의 객체를 새로 생성하여 root/person과 root/user에 동시에 삽입한다고 하자.
아래의 코드에 주석을 달아서 설명.
data class Person(
val sex: String = "",
val name: String = "",
val phoneNum: String = "",
val age: Int = 0
) {
fun toMap(): Map<String, Any?> { //객체를 데이터 베이스에 추가할 수 있도록 Map으로 변환하는 메서드.
return mapOf(
"sex" to sex,
"name" to name,
"phoneNum" to phoneNum,
"age" to age
)
}
}
private fun updateJSONObject() {
/*
push():
참조 노드의 자식 노드를 생성하고, 생성된 자식 노드의 참조를 반환한다.
따라서 "person" 아래에 새로운 자식 노드가 생성된다.
key:
해당 노드의 고유한 키를 반환한다. 따라서 "person" 아래에 생성된 자식 노드의 키가 반환된다.
생성되는 키는 Firebase에 의해 String? 타입의 무작위 값이 반환된다.
*/
val key = database.reference.child("person").push().key
if (key == null) {
Log.w(TAG, "Couldn't get push key for posts")
return
}
val person = Person("female", "Emma", "010-6666-7777", 18) //새로운 객체를 생성.
val personValues = person.toMap() //데이터 베이스에 추가할 수 있도록 Map으로 변환.
val newChildInsert = hashMapOf<String, Any>( //변경 내용을 HashMap으로 생성한다.
"/person/$key" to personValues, //person 아래 생긴 자식 노드에 값을 변경한다.
"/user/$key" to personValues //user 아래 새로운 자식 노드가 생성되고 값이 변경된다.
)
database.reference.updateChildren(newChildInsert)//해당 변경을 적용한다.
}
따라서 아래의 구조가 완성된다.
updateChildren() 메서드를 사용하여 복잡한 하위 구조에서 특정 다수 노드를 선택하여 한 번에 삭제할 수 있다. 아래의 구조에서 Catherine과 Michel의 데이터를 한 번에 삭제해보자.
아래의 코드에 주석을 달아서 설명.
private fun updateJSONObject() {
val updates = hashMapOf<String, Any?>( //변경 내용을 HashMap으로 생성한다.
"/person/1" to null, //Catherine의 노드인 person/1에 null을 삽입하여 삭제.
"/person/2" to null //Michel의 노드인 person/2에 null을 삽입하여 삭제.
)
database.reference.updateChildren(updates) //변경 사항 적용.
}
따라서 아래의 구조가 완성된다.
특정 참조 노드에서 removeValue() 메서드를 호출하면 해당 노드를 비롯한 자식 노드들이 삭제된다. 아래의 구조에서 root/user의 노드를 삭제하여 root/person만 남기고자 한다.
아래의 코드에 주석을 달아서 설명.
private fun removeJSONObject() {
database.reference.child("user").removeValue() //루트 노드 아래의 user 노드를 참조하여 해당 노드를 삭제.
}
따라서 아래의 구조가 완성된다.
removeEventListener()를 호출하여 특정 노드에 삽입된 리스너를 삭제할 수 있는데, 이는 삽입된 리스너 객체와 동일한 객체를 인자로 전달해야 하며 참조된 해당 노드만 적용되지 자식 노드는 적용되지 않는다. 또, 해당 참조 노드에 다수의 같은 리스너를 삽입하고 이를 완전히 삭제하길 원한다면 삽입한 횟수만큼의 호출이 요구된다.
val listener = object : ValueEventListener{
override fun onDataChange(snapshot: DataSnapshot) {
val person = snapshot.getValue<Person>()
}
override fun onCancelled(error: DatabaseError) {
TODO("Not yet implemented")
}
}
private fun pullJSONObject() {
reference2.addListenerForSingleValueEvent(listener)
}
private fun removeEventListener() {
reference2.removeEventListener(listener)
}
+트랜잭션, 원자적 증분을 지원하므로 필요시 찾아보기.
참고 :
https://firebase.google.com/docs/database/android/read-and-write?hl=ko#write_data https://firebase.google.com/docs/reference/kotlin/com/google/firebase/database/DatabaseReference#child(java.lang.String)
'IT > Android' 카테고리의 다른 글
Jetpack Compose - Swipe To Dismiss, Swipe to reveal with Realtime database (0) | 2024.05.14 |
---|---|
Android - Firebase Realtime Database Work with Lists (0) | 2024.05.08 |
Android - Firebase Realtime Database Structure (0) | 2024.05.07 |
App architecture/Dependency injection/Dependency injection with Hilt (0) | 2024.04.30 |
XML - ViewBinding, Customised Button In Android (0) | 2024.04.11 |