데이터 검색

이 문서에서는 데이터베이스 데이터 검색의 기초와 함께 데이터 정렬 방식 및 간단한 데이터 쿼리를 실행하는 방법을 설명합니다. Admin SDK의 데이터 검색은 프로그래밍 언어마다 약간 다르게 구현됩니다.

  1. 비동기 리스너: Firebase Realtime Database에 저장된 데이터는 데이터베이스 참조에 비동기 리스너를 연결하여 검색합니다. 리스너는 데이터의 초기 상태가 확인될 때 한 번 트리거된 후 데이터가 변경될 때마다 다시 트리거됩니다. 이벤트 리스너는 여러 가지 다양한 이벤트 유형을 수신할 수 있습니다. 이 데이터 검색 모드는 자바, Node.js, Python Admin SDK에서 지원됩니다.
  2. 블로킹 읽기: Firebase Realtime Database에 저장된 데이터는 데이터베이스 참조의 차단 메소드 호출을 통해 검색됩니다. 이 메소드는 참조에 저장된 데이터를 반환합니다. 각 메소드 호출은 일회성 작업입니다. 즉, SDK가 후속 데이터 업데이트를 수신 대기하는 콜백을 등록하지 않는다는 의미입니다. 이 데이터 검색 모델은 Python과 Go Admin SDK에서 지원됩니다.

시작하기

Firebase 데이터베이스에서 데이터를 읽는 방법을 알아보기 위해 앞 문서의 블로깅 예제를 다시 살펴보겠습니다. 예시 앱의 블로그 게시물은 데이터베이스 URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/posts.json에 저장됩니다. 게시물 데이터를 읽으려면 다음을 수행합니다.

자바
public static class Post {

  public String author;
  public String title;

  public Post(String author, String title) {
    // ...
  }

}

// Get a reference to our posts
final FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("server/saving-data/fireblog/posts");

// Attach a listener to read the data at our posts reference
ref.addValueEventListener(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot dataSnapshot) {
    Post post = dataSnapshot.getValue(Post.class);
    System.out.println(post);
  }

  @Override
  public void onCancelled(DatabaseError databaseError) {
    System.out.println("The read failed: " + databaseError.getCode());
  }
});
Node.js
// Get a database reference to our posts
const db = getDatabase();
const ref = db.ref('server/saving-data/fireblog/posts');

// Attach an asynchronous callback to read the data at our posts reference
ref.on('value', (snapshot) => {
  console.log(snapshot.val());
}, (errorObject) => {
  console.log('The read failed: ' + errorObject.name);
}); 
Python
# Import database module.
from firebase_admin import db

# Get a database reference to our posts
ref = db.reference('server/saving-data/fireblog/posts')

# Read the data at the posts reference (this is a blocking operation)
print(ref.get())
Go
// Post is a json-serializable type.
type Post struct {
	Author string `json:"author,omitempty"`
	Title  string `json:"title,omitempty"`
}

// Create a database client from App.
client, err := app.Database(ctx)
if err != nil {
	log.Fatalln("Error initializing database client:", err)
}

// Get a database reference to our posts
ref := client.NewRef("server/saving-data/fireblog/posts")

// Read the data at the posts reference (this is a blocking operation)
var post Post
if err := ref.Get(ctx, &post); err != nil {
	log.Fatalln("Error reading value:", err)
}

위 코드를 실행하면 모든 게시물이 포함된 객체가 Console에 로깅됩니다. Node.js와 자바의 경우 데이터베이스 참조에 새 데이터가 추가될 때마다 리스너 함수가 호출되며, 이 기능을 구현하기 위해 별도의 코드를 작성할 필요가 없습니다.

자바와 Node.js에서 콜백 함수는 데이터의 스냅샷인 DataSnapshot을 수신합니다. 스냅샷은 단일 시점에 특정 데이터베이스 참조에 있던 데이터를 촬영한 사진과 같습니다. 스냅샷에서 val()/getValue()를 호출하면 데이터의 언어별 객체 표현이 반환됩니다. 참조 위치에 데이터가 없으면 스냅샷의 값은 null입니다. Python의 get() 메서드는 데이터의 Python 표현을 직접 반환합니다. Go의 Get() 함수는 데이터를 지정된 데이터 구조로 마셜링 취소합니다.

위 예시에서는 value 이벤트 유형을 사용했는데, 이 유형은 데이터가 하나만 변경되어도 Firebase 데이터베이스 참조의 전체 콘텐츠를 읽습니다. value는 데이터베이스에서 데이터를 읽는 데 사용할 수 있는 아래의 5가지 이벤트 유형 중 하나입니다.

자바 및 Node.js의 읽기 이벤트 유형

value 이벤트는 읽기 이벤트 발생 시점에 특정 데이터베이스 경로에 있던 내용의 정적 스냅샷을 읽는 데 사용됩니다. 이 이벤트는 초기 데이터가 확인될 때 한 번 발생한 후 데이터가 변경될 때마다 발생합니다. 하위 데이터를 포함하여 해당 위치의 모든 데이터를 포함하는 스냅샷이 이벤트 콜백에 전달됩니다. 위의 코드 예시에서 value는 앱의 모든 블로그 게시물을 반환했습니다. 새 블로그 게시물이 추가될 때마다 콜백 함수가 모든 게시물을 반환합니다.

하위 항목 추가

child_added 이벤트는 일반적으로 데이터베이스에서 항목 목록을 검색하는 데 사용됩니다. 위치의 전체 콘텐츠를 반환하는 value와 달리 child_added는 기존 하위 항목마다 한 번씩 트리거된 후 지정된 경로에 하위 항목이 새로 추가될 때마다 다시 트리거됩니다. 새 하위 항목의 데이터를 포함하는 스냅샷이 이벤트 콜백에 전달됩니다. 정렬을 위해 이전 하위 항목의 키를 포함하는 두 번째 인수도 전달됩니다.

블로그 앱에 새로 추가된 각 게시물의 데이터만 검색하려면 child_added를 사용합니다.

자바
ref.addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    Post newPost = dataSnapshot.getValue(Post.class);
    System.out.println("Author: " + newPost.author);
    System.out.println("Title: " + newPost.title);
    System.out.println("Previous Post ID: " + prevChildKey);
  }

  @Override
  public void onChildChanged(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onChildRemoved(DataSnapshot dataSnapshot) {}

  @Override
  public void onChildMoved(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onCancelled(DatabaseError databaseError) {}
});
Node.js
// Retrieve new posts as they are added to our database
ref.on('child_added', (snapshot, prevChildKey) => {
  const newPost = snapshot.val();
  console.log('Author: ' + newPost.author);
  console.log('Title: ' + newPost.title);
  console.log('Previous Post ID: ' + prevChildKey);
});

이 예시에서 스냅샷은 개별 블로그 게시물이 있는 객체를 포함합니다. SDK는 값을 검색하여 게시물을 객체로 변환하므로 authortitle을 각각 호출하여 게시물의 저자 및 제목 속성에 액세스할 수 있습니다. 또한 두 번째 prevChildKey 인수에서 이전 게시물 ID에 액세스할 수 있습니다.

하위 변경

하위 노드가 수정될 때마다 child_changed 이벤트가 트리거됩니다. 여기에는 하위 노드의 하위 항목에 대한 수정이 포함됩니다. 이 이벤트는 일반적으로 child_addedchild_removed와 함께 항목 목록의 변경에 대응하는 데 사용됩니다. 이벤트 콜백에 전달되는 스냅샷에는 하위 항목의 업데이트된 데이터가 포함됩니다.

블로그 게시물이 수정되면 child_changed를 사용하여 업데이트된 데이터를 읽을 수 있습니다.

자바
ref.addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onChildChanged(DataSnapshot dataSnapshot, String prevChildKey) {
    Post changedPost = dataSnapshot.getValue(Post.class);
    System.out.println("The updated post title is: " + changedPost.title);
  }

  @Override
  public void onChildRemoved(DataSnapshot dataSnapshot) {}

  @Override
  public void onChildMoved(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onCancelled(DatabaseError databaseError) {}
});
Node.js
// Get the data on a post that has changed
ref.on('child_changed', (snapshot) => {
  const changedPost = snapshot.val();
  console.log('The updated post title is ' + changedPost.title);
});

하위 항목 삭제

child_removed 이벤트는 바로 아래 하위 항목이 삭제될 때 트리거됩니다. 일반적으로 child_addedchild_changed와 함께 사용됩니다. 이벤트 콜백에 전달되는 스냅샷에는 삭제된 하위 항목의 데이터가 포함됩니다.

블로그 예시에서 child_removed를 사용하여 삭제된 게시물에 대한 알림을 Console에 로깅할 수 있습니다.

자바
ref.addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onChildChanged(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onChildRemoved(DataSnapshot dataSnapshot) {
    Post removedPost = dataSnapshot.getValue(Post.class);
    System.out.println("The blog post titled " + removedPost.title + " has been deleted");
  }

  @Override
  public void onChildMoved(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onCancelled(DatabaseError databaseError) {}
});
Node.js
// Get a reference to our posts
const ref = db.ref('server/saving-data/fireblog/posts');

// Get the data on a post that has been removed
ref.on('child_removed', (snapshot) => {
  const deletedPost = snapshot.val();
  console.log('The blog post titled \'' + deletedPost.title + '\' has been deleted');
});

하위 항목 이동

child_moved 이벤트는 정렬된 데이터를 다룰 때 사용되며 자세한 내용은 다음 섹션에서 다룹니다.

이벤트 보증

Firebase 데이터베이스는 이벤트와 관련하여 몇 가지 중요한 사항을 보증합니다.

데이터베이스 이벤트 보증
로컬 상태가 변경되면 항상 이벤트가 발생합니다.
네트워크 연결이 잠시 끊길 때와 같이 로컬 작업 또는 타이밍으로 인해 일시적인 차이가 발생하더라도 이벤트는 결과적으로 항상 데이터의 올바른 상태를 반영합니다.
단일 클라이언트의 쓰기 작업은 항상 서버에 기록된 후에 다른 사용자에게 전파됩니다.
값 이벤트는 항상 마지막에 발생하며, 스냅샷이 생성되기 전에 발생한 모든 기타 이벤트의 업데이트를 항상 포함합니다.

값 이벤트는 항상 마지막에 발생하므로 다음 예제는 항상 정상적으로 작동합니다.

자바
final AtomicInteger count = new AtomicInteger();

ref.addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    // New child added, increment count
    int newCount = count.incrementAndGet();
    System.out.println("Added " + dataSnapshot.getKey() + ", count is " + newCount);
  }

  // ...
});

// The number of children will always be equal to 'count' since the value of
// the dataSnapshot here will include every child_added event triggered before this point.
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot dataSnapshot) {
    long numChildren = dataSnapshot.getChildrenCount();
    System.out.println(count.get() + " == " + numChildren);
  }

  @Override
  public void onCancelled(DatabaseError databaseError) {}
});
Node.js
let count = 0;

ref.on('child_added', (snap) => {
  count++;
  console.log('added:', snap.key);
});

// length will always equal count, since snap.val() will include every child_added event
// triggered before this point
ref.once('value', (snap) => {
  console.log('initial data loaded!', snap.numChildren() === count);
});

콜백 분리

콜백을 삭제하려면 다음과 같이 이벤트 유형 및 삭제할 콜백 함수를 지정합니다.

자바
// Create and attach listener
ValueEventListener listener = new ValueEventListener() {
    // ...
};
ref.addValueEventListener(listener);

// Remove listener
ref.removeEventListener(listener);
Node.js
ref.off('value', originalCallback);

범위 컨텍스트를 on()에 전달한 경우 콜백을 분리할 때도 전달해야 합니다.

자바
// Not applicable for Java
Node.js
ref.off('value', originalCallback, ctx);

특정 위치의 모든 콜백을 삭제하려면 다음을 수행합니다.

자바
// No Java equivalent, listeners must be removed individually.
Node.js
// Remove all value callbacks
ref.off('value');

// Remove all callbacks of any type
ref.off();

데이터 한번 읽기

한 번만 호출되고 즉시 삭제되는 콜백이 유용한 경우가 있습니다. 편의를 위해 다음과 같은 헬퍼 함수를 만들었습니다.

자바
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot dataSnapshot) {
    // ...
  }

  @Override
  public void onCancelled(DatabaseError databaseError) {
    // ...
  }
});
Node.js
ref.once('value', (data) => {
  // do some stuff once
});
Python
# Import database module.
from firebase_admin import db

# Get a database reference to our posts
ref = db.reference('server/saving-data/fireblog/posts')

# Read the data at the posts reference (this is a blocking operation)
print(ref.get())
Go
// Create a database client from App.
client, err := app.Database(ctx)
if err != nil {
	log.Fatalln("Error initializing database client:", err)
}

// Get a database reference to our posts
ref := client.NewRef("server/saving-data/fireblog/posts")

// Read the data at the posts reference (this is a blocking operation)
var post Post
if err := ref.Get(ctx, &post); err != nil {
	log.Fatalln("Error reading value:", err)
}

데이터 쿼리

Firebase 데이터베이스 쿼리를 사용하여 다양한 요소를 기준으로 데이터를 선택적으로 검색할 수 있습니다. 데이터베이스에서 쿼리를 작성하려면 먼저 정렬 함수(orderByChild(), orderByKey() 또는 orderByValue()) 중 하나를 사용하여 데이터를 정렬하는 방법을 지정합니다. 그런 다음 5가지 기타 메서드(limitToFirst(), limitToLast(), startAt(), endAt(), equalTo())와 조합하여 복잡한 쿼리를 수행할 수 있습니다.

공룡을 좋아하는 Firebase 개발팀의 취향에 따라 공룡 자료 샘플 데이터베이스의 스니펫을 사용하여 Firebase 데이터베이스에서 데이터를 쿼리하는 방법을 시연해 보겠습니다.

{
  "lambeosaurus": {
    "height" : 2.1,
    "length" : 12.5,
    "weight": 5000
  },
  "stegosaurus": {
    "height" : 4,
    "length" : 9,
    "weight" : 2500
  }
}

데이터를 정렬할 수 있는 3가지 기준은 하위 키, , 입니다. 기본적인 데이터베이스 쿼리는 이러한 정렬 함수 중 하나로 시작되며, 아래에서 이러한 함수를 각각 설명합니다.

지정된 하위 키로 정렬

공통 하위 키를 orderByChild()에 전달하여 그 키를 기준으로 노드를 정렬할 수 있습니다. 예를 들어 모든 공룡을 신장을 기준으로 정렬하여 읽으려면 다음을 수행합니다.

자바
public static class Dinosaur {

  public int height;
  public int weight;

  public Dinosaur(int height, int weight) {
    // ...
  }

}

final DatabaseReference dinosaursRef = database.getReference("dinosaurs");
dinosaursRef.orderByChild("height").addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    Dinosaur dinosaur = dataSnapshot.getValue(Dinosaur.class);
    System.out.println(dataSnapshot.getKey() + " was " + dinosaur.height + " meters tall.");
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');

ref.orderByChild('height').on('child_added', (snapshot) => {
  console.log(snapshot.key + ' was ' + snapshot.val().height + ' meters tall');
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_child('height').get()
for key, val in snapshot.items():
    print('{0} was {1} meters tall'.format(key, val))
Go
// Dinosaur is a json-serializable type.
type Dinosaur struct {
	Height int `json:"height"`
	Width  int `json:"width"`
}

ref := client.NewRef("dinosaurs")

results, err := ref.OrderByChild("height").GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	var d Dinosaur
	if err := r.Unmarshal(&d); err != nil {
		log.Fatalln("Error unmarshaling result:", err)
	}
	fmt.Printf("%s was %d meteres tall", r.Key(), d.Height)
}

쿼리 대상인 하위 키가 없는 노드는 null 값을 기준으로 정렬되어 가장 먼저 나오게 됩니다. 데이터가 정렬되는 방식에 대한 자세한 내용은 데이터 정렬 방식 섹션을 참조하세요.

바로 아래 수준의 하위 항목뿐 아니라 깊이 중첩된 하위 항목을 기준으로 정렬할 수도 있습니다. 아래와 같이 데이터가 깊이 중첩된 경우 이 기능이 유용합니다.

{
  "lambeosaurus": {
    "dimensions": {
      "height" : 2.1,
      "length" : 12.5,
      "weight": 5000
    }
  },
  "stegosaurus": {
    "dimensions": {
      "height" : 4,
      "length" : 9,
      "weight" : 2500
    }
  }
}

이러한 경우 키를 쿼리하려면 단일 키가 아닌 개체에 대한 전체 경로를 사용합니다.

자바
dinosaursRef.orderByChild("dimensions/height").addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    // ...
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');
ref.orderByChild('dimensions/height').on('child_added', (snapshot) => {
  console.log(snapshot.key + ' was ' + snapshot.val().height + ' meters tall');
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_child('dimensions/height').get()
for key, val in snapshot.items():
    print('{0} was {1} meters tall'.format(key, val))
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByChild("dimensions/height").GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	var d Dinosaur
	if err := r.Unmarshal(&d); err != nil {
		log.Fatalln("Error unmarshaling result:", err)
	}
	fmt.Printf("%s was %d meteres tall", r.Key(), d.Height)
}

쿼리에서 한 번에 하나의 키로만 정렬할 수 있습니다. 한 쿼리에서 orderByChild()를 여러 번 호출하면 오류가 발생합니다.

키로 정렬

orderByKey() 메서드를 사용하여 키를 기준으로 노드를 정렬할 수도 있습니다. 다음 예시에서는 모든 공룡을 알파벳 순으로 읽습니다.

자바
dinosaursRef.orderByKey().addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
var ref = db.ref('dinosaurs');
ref.orderByKey().on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_key().get()
print(snapshot)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByKey().GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
snapshot := make([]Dinosaur, len(results))
for i, r := range results {
	var d Dinosaur
	if err := r.Unmarshal(&d); err != nil {
		log.Fatalln("Error unmarshaling result:", err)
	}
	snapshot[i] = d
}
fmt.Println(snapshot)

값으로 정렬

orderByValue() 메서드를 사용하여 하위 키 값을 기준으로 노드를 정렬할 수 있습니다. 공룡들이 운동경기를 한 다음 점수를 다음과 같은 형식으로 기록한다고 가정해 보겠습니다.

{
  "scores": {
    "bruhathkayosaurus" : 55,
    "lambeosaurus" : 21,
    "linhenykus" : 80,
    "pterodactyl" : 93,
    "stegosaurus" : 5,
    "triceratops" : 22
  }
}

공룡들을 점수에 따라 정렬하려면 다음과 같은 쿼리를 작성합니다.

자바
DatabaseReference scoresRef = database.getReference("scores");
scoresRef.orderByValue().addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println("The " + dataSnapshot.getKey() + " score is " + dataSnapshot.getValue());
  }

  // ...
});
Node.js
const scoresRef = db.ref('scores');
scoresRef.orderByValue().on('value', (snapshot) => {
  snapshot.forEach((data) => {
    console.log('The ' + data.key + ' dinosaur\'s score is ' + data.val());
  });
});
Python
ref = db.reference('scores')
snapshot = ref.order_by_value().get()
for key, val in snapshot.items():
    print('The {0} dinosaur\'s score is {1}'.format(key, val))
Go
ref := client.NewRef("scores")

results, err := ref.OrderByValue().GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	var score int
	if err := r.Unmarshal(&score); err != nil {
		log.Fatalln("Error unmarshaling result:", err)
	}
	fmt.Printf("The %s dinosaur's score is %d\n", r.Key(), score)
}

orderByValue()를 사용할 때 null, 부울, 문자열, 객체 값이 정렬되는 방식에 대한 설명은 데이터 정렬 방식 섹션을 참조하세요.

복잡한 쿼리

이제 데이터 정렬 방식을 명확히 이해했으므로 아래와 같이 제한 또는 범위 메소드를 사용하여 더욱 복잡한 쿼리를 작성할 수 있습니다.

제한 쿼리

limitToFirst()limitToLast() 쿼리를 사용하여 특정 콜백에서 동기화할 하위 항목의 최대 개수를 설정합니다. 제한을 100개로 설정하면 처음에 최대 100개의 child_added 이벤트만 수신합니다. 데이터베이스에 저장된 메시지가 100개 미만이면 각 메시지마다 child_added 이벤트가 발생합니다. 하지만 메시지가 100개 이상이면 메시지 중 100개에 대한 child_added 이벤트가 수신됩니다. limitToFirst()를 사용하는 경우 정렬된 처음 100개의 메시지가, limitToLast()를 사용하는 경우 정렬된 마지막 100개의 메시지가 수신됩니다. 항목이 변경됨에 따라 쿼리에 새로 포함되는 항목에 대한 child_added 이벤트와 쿼리에서 제외되는 항목에 대한 child_removed 이벤트가 수신되며 총 개수는 100개로 유지됩니다.

공룡 자료 데이터베이스와 orderByChild()를 사용하여 가장 무거운 두 공룡을 찾을 수 있습니다.

자바
dinosaursRef.orderByChild("weight").limitToLast(2).addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');
ref.orderByChild('weight').limitToLast(2).on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_child('weight').limit_to_last(2).get()
for key in snapshot:
    print(key)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByChild("weight").LimitToLast(2).GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	fmt.Println(r.Key())
}

데이터베이스에 저장된 공룡이 둘 이상이면 child_added 콜백이 정확히 두 번 호출됩니다. 또한 데이터베이스에 더 무거운 공룡이 새로 추가될 때마다 호출됩니다. Python에서 이 쿼리는 가장 무거운 두 공룡이 포함된 OrderedDict를 바로 반환합니다.

마찬가지로 limitToFirst()를 사용하여 키가 가장 작은 두 공룡을 찾을 수 있습니다.

자바
dinosaursRef.orderByChild("weight").limitToFirst(2).addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');
ref.orderByChild('height').limitToFirst(2).on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_child('height').limit_to_first(2).get()
for key in snapshot:
    print(key)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByChild("height").LimitToFirst(2).GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	fmt.Println(r.Key())
}

데이터베이스에 저장된 공룡이 둘 이상이면 child_added 콜백이 정확히 두 번 호출됩니다. 처음 두 공룡 중 하나가 데이터베이스에서 삭제되면 새 공룡이 해당 위치를 차지하므로 콜백이 다시 호출됩니다. Python에서 이 쿼리는 가장 키가 작은 공룡이 포함된 OrderedDict를 바로 반환합니다.

orderByValue()로 제한 쿼리를 수행할 수도 있습니다. 공룡 운동경기 점수 1, 2, 3위를 포함하는 리더보드를 만들려면 다음 쿼리를 실행합니다.

자바
scoresRef.orderByValue().limitToFirst(3).addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println("The " + dataSnapshot.getKey() + " score is " + dataSnapshot.getValue());
  }

  // ...
});
Node.js
const scoresRef = db.ref('scores');
scoresRef.orderByValue().limitToLast(3).on('value', (snapshot)  =>{
  snapshot.forEach((data) => {
    console.log('The ' + data.key + ' dinosaur\'s score is ' + data.val());
  });
});
Python
scores_ref = db.reference('scores')
snapshot = scores_ref.order_by_value().limit_to_last(3).get()
for key, val in snapshot.items():
    print('The {0} dinosaur\'s score is {1}'.format(key, val))
Go
ref := client.NewRef("scores")

results, err := ref.OrderByValue().LimitToLast(3).GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	var score int
	if err := r.Unmarshal(&score); err != nil {
		log.Fatalln("Error unmarshaling result:", err)
	}
	fmt.Printf("The %s dinosaur's score is %d\n", r.Key(), score)
}

범위 쿼리

startAt(), endAt(), equalTo()를 사용하면 쿼리의 시작 및 종료 지점을 임의로 선택할 수 있습니다. 예를 들어 키가 3미터 이상인 모든 공룡을 찾으려면 orderByChild()startAt()을 조합합니다.

자바
dinosaursRef.orderByChild("height").startAt(3).addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');
ref.orderByChild('height').startAt(3).on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_child('height').start_at(3).get()
for key in snapshot:
    print(key)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByChild("height").StartAt(3).GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	fmt.Println(r.Key())
}

endAt()을 사용하여 사전순으로 이름이 Pterodactyl보다 먼저 나오는 공룡을 모두 찾을 수 있습니다.

자바
dinosaursRef.orderByKey().endAt("pterodactyl").addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');
ref.orderByKey().endAt('pterodactyl').on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_key().end_at('pterodactyl').get()
for key in snapshot:
    print(key)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByKey().EndAt("pterodactyl").GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	fmt.Println(r.Key())
}

startAt()endAt()을 조합하여 쿼리의 양끝을 제한할 수 있습니다. 다음 예시에서는 이름이 'b'로 시작되는 공룡을 모두 검색합니다.

자바
dinosaursRef.orderByKey().startAt("b").endAt("b\uf8ff").addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
var ref = db.ref('dinosaurs');
ref.orderByKey().startAt('b').endAt('b\uf8ff').on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_key().start_at('b').end_at(u'b\uf8ff').get()
for key in snapshot:
    print(key)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByKey().StartAt("b").EndAt("b\uf8ff").GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	fmt.Println(r.Key())
}

equalTo() 메서드를 사용하면 정확히 일치하는 항목을 필터링할 수 있습니다. 다른 범위 쿼리와 마찬가지로 일치하는 각 하위 노드마다 실행됩니다. 예를 들어 다음 쿼리를 사용하여 키가 25미터인 공룡을 모두 찾을 수 있습니다.

자바
dinosaursRef.orderByChild("height").equalTo(25).addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');
ref.orderByChild('height').equalTo(25).on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_child('height').equal_to(25).get()
for key in snapshot:
    print(key)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByChild("height").EqualTo(25).GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	fmt.Println(r.Key())
}

범위 쿼리는 데이터를 페이지화하는 경우에도 유용합니다.

하나로 결합

이러한 기법을 모두 조합하여 복잡한 쿼리를 만들 수 있습니다. 예를 들어 Stegosaurus보다 키가 작은 공룡의 이름을 검색할 수 있습니다.

자바
dinosaursRef.child("stegosaurus").child("height").addValueEventListener(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot stegoHeightSnapshot) {
    Integer favoriteDinoHeight = stegoHeightSnapshot.getValue(Integer.class);
    Query query = dinosaursRef.orderByChild("height").endAt(favoriteDinoHeight).limitToLast(2);
    query.addValueEventListener(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot dataSnapshot) {
        // Data is ordered by increasing height, so we want the first entry
        DataSnapshot firstChild = dataSnapshot.getChildren().iterator().next();
        System.out.println("The dinosaur just shorter than the stegosaurus is: " + firstChild.getKey());
      }

      @Override
      public void onCancelled(DatabaseError databaseError) {
        // ...
      }
    });
  }

  @Override
  public void onCancelled(DatabaseError databaseError) {
    // ...
  }
});
Node.js
  const ref = db.ref('dinosaurs');
  ref.child('stegosaurus').child('height').on('value', (stegosaurusHeightSnapshot) => {
    const favoriteDinoHeight = stegosaurusHeightSnapshot.val();

    const queryRef = ref.orderByChild('height').endAt(favoriteDinoHeight).limitToLast(2);
    queryRef.on('value', (querySnapshot) => {
      if (querySnapshot.numChildren() === 2) {
        // Data is ordered by increasing height, so we want the first entry
        querySnapshot.forEach((dinoSnapshot) => {
          console.log('The dinosaur just shorter than the stegasaurus is ' + dinoSnapshot.key);

          // Returning true means that we will only loop through the forEach() one time
          return true;
        });
      } else {
        console.log('The stegosaurus is the shortest dino');
      }
    });
});
Python
ref = db.reference('dinosaurs')
favotire_dino_height = ref.child('stegosaurus').child('height').get()
query = ref.order_by_child('height').end_at(favotire_dino_height).limit_to_last(2)
snapshot = query.get()
if len(snapshot) == 2:
    # Data is ordered by increasing height, so we want the first entry.
    # Second entry is stegosarus.
    for key in snapshot:
        print('The dinosaur just shorter than the stegosaurus is {0}'.format(key))
        return
else:
    print('The stegosaurus is the shortest dino')
Go
ref := client.NewRef("dinosaurs")

var favDinoHeight int
if err := ref.Child("stegosaurus").Child("height").Get(ctx, &favDinoHeight); err != nil {
	log.Fatalln("Error querying database:", err)
}

query := ref.OrderByChild("height").EndAt(favDinoHeight).LimitToLast(2)
results, err := query.GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
if len(results) == 2 {
	// Data is ordered by increasing height, so we want the first entry.
	// Second entry is stegosarus.
	fmt.Printf("The dinosaur just shorter than the stegosaurus is %s\n", results[0].Key())
} else {
	fmt.Println("The stegosaurus is the shortest dino")
}

데이터 정렬 방식

이 섹션에서는 4가지 정렬 함수를 사용할 때 데이터가 정렬되는 방식을 각각 설명합니다.

orderByChild

orderByChild()를 사용하면 지정된 하위 키를 포함하는 데이터가 다음과 같이 정렬됩니다.

  1. 지정된 하위 키의 값이 null인 하위 항목이 맨 처음에 옵니다.
  2. 지정된 하위 키의 값이 false인 하위 항목이 그 다음에 옵니다. 값이 false인 하위 항목이 여러 개인 경우 키에 따라 사전순으로 정렬됩니다.
  3. 지정된 하위 키의 값이 true인 하위 항목이 그 다음에 옵니다. 값이 true인 하위 항목이 여러 개인 경우 키에 따라 사전순으로 정렬됩니다.
  4. 숫자 값을 갖는 하위 항목이 다음에 나오며 오름차순으로 정렬됩니다. 지정된 하위 노드의 숫자 값이 동일한 하위 항목이 여러 개이면 키에 따라 정렬됩니다.
  5. 숫자 다음에는 문자열이 나오며 사전순, 오름차순으로 정렬됩니다. 지정된 하위 노드의 값이 동일한 하위 항목이 여러 개이면 키에 따라 사전순으로 정렬됩니다.
  6. 마지막으로 개체가 나오며 키에 따라 사전순, 오름차순으로 정렬됩니다.

orderByKey

orderByKey()를 사용하여 데이터를 정렬하면 데이터가 키에 따라 오름차순으로 반환됩니다. 키는 항상 문자열입니다.

  1. 키가 32비트 정수로 파싱될 수 있는 하위 항목이 맨 처음에 나오며 오름차순으로 정렬됩니다.
  2. 키가 문자열 값인 하위 항목이 다음에 나오며 사전순, 오름차순으로 정렬됩니다.

orderByValue

orderByValue()를 사용하면 하위 항목이 값에 따라 정렬됩니다. 정렬 기준은 orderByChild()와 동일하며, 지정된 하위 키의 값 대신 노드의 값이 사용된다는 점이 다릅니다.