Соединения с представлениями
Связанные документы
Если ваша функция map выдаёт объектное значение, которое содержит {'_id': XXX}, и вы запрашиваете представление с параметром include_docs=true, CouchDB получит документ с идентификатором XXX, а не документ, который обрабатывался при выдаче пары ключ/значение.
Это означает, что если один документ содержит идентификаторы других документов, эти документы можно также получить в представлении, разместив их рядом с тем же ключом, если это требуется.
Например, если у вас есть следующие документы, связанные иерархически:
[
{ "_id": "11111" },
{ "_id": "22222", "ancestors": ["11111"], "value": "hello" },
{ "_id": "33333", "ancestors": ["22222","11111"], "value": "world" }
] Вы можете выдавать значения вместе с документами-предками, разместив их рядом в представлении, вот так:
function(doc) {
if (doc.value) {
emit([doc.value, 0], null);
if (doc.ancestors) {
for (var i in doc.ancestors) {
emit([doc.value, Number(i)+1], {_id: doc.ancestors[i]});
}
}
}
} В результате вы получите:
{
"total_rows": 5,
"offset": 0,
"rows": [
{
"id": "22222",
"key": [
"hello",
0
],
"value": null,
"doc": {
"_id": "22222",
"_rev": "1-0eee81fecb5aa4f51e285c621271ff02",
"ancestors": [
"11111"
],
"value": "hello"
}
},
{
"id": "22222",
"key": [
"hello",
1
],
"value": {
"_id": "11111"
},
"doc": {
"_id": "11111",
"_rev": "1-967a00dff5e02add41819138abb3284d"
}
},
{
"id": "33333",
"key": [
"world",
0
],
"value": null,
"doc": {
"_id": "33333",
"_rev": "1-11e42b44fdb3d3784602eca7c0332a43",
"ancestors": [
"22222",
"11111"
],
"value": "world"
}
},
{
"id": "33333",
"key": [
"world",
1
],
"value": {
"_id": "22222"
},
"doc": {
"_id": "22222",
"_rev": "1-0eee81fecb5aa4f51e285c621271ff02",
"ancestors": [
"11111"
],
"value": "hello"
}
},
{
"id": "33333",
"key": [
"world",
2
],
"value": {
"_id": "11111"
},
"doc": {
"_id": "11111",
"_rev": "1-967a00dff5e02add41819138abb3284d"
}
}
]
} что позволяет очень дёшево получить документ и всех его предков одним запросом.
Обратите внимание, что "id" в строке по-прежнему относится к исходному документу. Разница лишь в том, что include_docs получает другой документ.
Текущая ревизия документа определяется во время выполнения запроса, а не в момент генерации представления. Это означает, что если позднее будет добавлена новая ревизия связанного документа, она появится в запросах к представлению, хотя само представление не изменилось. Чтобы использовать конкретную ревизию связанного документа, выдавайте свойство "_rev" вместе с "_id".
Использование сортировки представлений
- Автор:
-
Christopher Lenz
- Дата:
-
2007-10-05
- Источник:
Сегодня в IRC обсуждали, как смоделировать простую систему блогов с сущностями «публикация» и «комментарий», где у каждой публикации в блоге может быть N комментариев. Если бы вы использовали базу данных SQL, у вас, очевидно, было бы две таблицы с внешними ключами и вы бы использовали соединения. (По крайней мере до тех пор, пока не понадобилась бы денормализация.)
Но как бы выглядел «очевидный» подход в CouchDB?
Подход № 1: комментарии внутри документа
Простой подход — создать один документ на каждую публикацию в блоге и хранить комментарии внутри этого документа:
{
"_id": "myslug",
"_rev": "123456",
"author": "john",
"title": "My blog post",
"content": "Bla bla bla …",
"comments": [
{"author": "jack", "content": "…"},
{"author": "jane", "content": "…"}
]
} Примечание
Разумеется, модель реальной системы блогов была бы более сложной: в ней были бы теги, временные метки и так далее. Здесь мы лишь демонстрируем основы.
Очевидное преимущество такого подхода в том, что связанные данные хранятся в одном месте. Удалите публикацию — и соответствующие комментарии также удалятся автоматически.
Возможно, вы думаете, что хранение комментариев внутри документа публикации не позволит запрашивать сами комментарии, но это не так. Можно без труда написать представление CouchDB, которое будет возвращать все комментарии из всех публикаций в блоге с ключом, соответствующим автору:
function(doc) {
for (var i in doc.comments) {
emit(doc.comments[i].author, doc.comments[i].content);
}
} Теперь можно получить список всех комментариев определённого пользователя, вызвав представление и передав ему параметр строки запроса ?key="username".
Однако у этого подхода есть недостаток, который может быть весьма существенным для многих приложений. Чтобы добавить комментарий к публикации, нужно:
Получить документ публикации в блоге
Добавить новый комментарий в структуру JSON
Отправить обновлённый документ на сервер
Если несколько клиентских процессов добавляют комментарии примерно в одно и то же время, некоторые из них на шаге 3 получат ошибку HTTP 409 Conflict (так проявляется оптимистичный контроль параллелизма). Для некоторых приложений это приемлемо, но во многих других хотелось бы добавлять новые связанные данные независимо от того, были ли тем временем добавлены другие данные.
Единственный способ добавлять связанные данные без конфликтов — поместить их в отдельные документы.
Подход № 2: комментарии отдельно
При таком подходе для каждой публикации в блоге создаётся отдельный документ, а для каждого комментария — свой документ. В документах комментариев будет обратная ссылка на публикацию, к которой они относятся.
Документ публикации будет похож на приведённый выше, но без свойства comments. Кроме того, теперь во всех документах будет свойство type, позволяющее различать публикации и комментарии:
{
"_id": "myslug",
"_rev": "123456",
"type": "post",
"author": "john",
"title": "My blog post",
"content": "Bla bla bla …"
} Сами комментарии хранятся в отдельных документах, в которых также есть свойство type (на этот раз со значением «comment»), а кроме того — свойство post, содержащее идентификатор документа публикации, к которой они относятся:
{
"_id": "ABCDEF",
"_rev": "123456",
"type": "comment",
"post": "myslug",
"author": "jack",
"content": "…"
} {
"_id": "DEFABC",
"_rev": "123456",
"type": "comment",
"post": "myslug",
"author": "jane",
"content": "…"
} Чтобы перечислить все комментарии к каждой публикации в блоге, добавим простое представление с ключом, равным идентификатору публикации:
function(doc) {
if (doc.type == "comment") {
emit(doc.post, {author: doc.author, content: doc.content});
}
} И вызовем это представление, передав ему параметр строки запроса ?key="post_id".
Получить все комментарии автора так же просто, как и раньше:
function(doc) {
if (doc.type == "comment") {
emit(doc.author, {post: doc.post, content: doc.content});
}
} Таким образом, в некоторых отношениях этот подход лучше, но у него есть и недостаток. Представьте, что вы хотите отобразить публикацию в блоге вместе со всеми связанными комментариями на одной веб-странице. В первом подходе нам нужен был всего один запрос к серверу CouchDB — запрос GET к документу. Во втором подходе нужны два запроса: запрос GET к документу публикации и запрос GET к представлению, возвращающему все комментарии к публикации.
Это допустимо, но не совсем удобно. Представьте, что вы захотели добавить древовидные комментарии: теперь пришлось бы отдельно получать каждый комментарий. Вероятно, нам нужен способ объединить публикацию в блоге и различные комментарии, чтобы получить их одним HTTP-запросом.
Именно тогда Дэмиен Кац, автор CouchDB, присоединился к обсуждению в IRC и показал нам решение.
Оптимизация: используем возможности сортировки представлений
Для Дэмиена это было очевидно, но для остальных — совсем нет: довольно просто создать представление, включающее содержимое документа публикации в блоге и содержимое всех связанных с ней комментариев. Для этого нужно использовать составные ключи. До сих пор мы использовали в качестве ключей представления простые строковые значения, но на самом деле ими могут быть произвольные значения JSON. Давайте этим воспользуемся:
function(doc) {
if (doc.type == "post") {
emit([doc._id, 0], null);
} else if (doc.type == "comment") {
emit([doc.post, 1], null);
}
} Поначалу это может сбить с толку. Давайте немного отступим и посмотрим, что именно представляют собой представления CouchDB.
По сути, представления CouchDB — это высокоэффективные дисковые словари, сопоставляющие ключи со значениями. Ключ автоматически индексируется и может использоваться для фильтрации и/или сортировки результатов, возвращаемых представлениями. При вызове представления можно указать, что вас интересует только часть строк, передав параметр строки запроса ?key=foo. Также можно указать параметры строки запроса ?startkey=foo и/или ?endkey=bar, чтобы получить строки в диапазоне ключей. Наконец, добавив к запросу ?include_docs=true, вы получите полное содержимое каждого выданного документа.
Важно также отметить, что ключи всегда используются для упорядочения строк (то есть их сортировки). В CouchDB определены правила (пока не задокументированные) сравнения произвольных объектов JSON при сортировке. Например, значение JSON ["foo", 2] сортируется после (считается «большим, чем») значений ["foo"] или ["foo", 1, "bar"], но перед, например, ["foo", 2, "bar"]. Эта возможность позволяет использовать целый ряд неочевидных приёмов…
См. также
Имея это в виду, вернёмся к приведённой выше функции представления. Прежде всего обратите внимание: в отличие от предыдущих функций представлений, использованных здесь, эта функция обрабатывает и документы «post», и документы «comment», и оба типа попадают в одно и то же представление. Кроме того, ключ в этом представлении — не простая строка, а массив. Первый элемент массива всегда является идентификатором публикации, независимо от того, обрабатываем ли мы сам документ публикации или связанный с ней комментарий. Второй элемент равен 0 для документов публикаций и 1 для документов комментариев.
Предположим, что в нашей базе данных есть две публикации в блоге. Если не ограничивать результаты представления с помощью key, startkey или endkey, мы получим примерно следующее:
{
"total_rows": 5, "offset": 0, "rows": [{
"id": "myslug",
"key": ["myslug", 0],
"value": null
}, {
"id": "ABCDEF",
"key": ["myslug", 1],
"value": null
}, {
"id": "DEFABC",
"key": ["myslug", 1],
"value": null
}, {
"id": "other_slug",
"key": ["other_slug", 0],
"value": null
}, {
"id": "CDEFAB",
"key": ["other_slug", 1],
"value": null
},
]
} Примечание
Здесь заполнители ... будут содержать полное JSON-представление соответствующих документов
Чтобы получить конкретную публикацию в блоге и все связанные с ней комментарии, вызовем это представление со следующей строкой запроса:
?startkey=["myslug"]&endkey=["myslug", 2]&include_docs=true
Мы получим первые три строки, относящиеся к публикации myslug, но не остальные, а также полное содержимое каждого документа. Voilà — теперь у нас есть все данные, необходимые для отображения публикации вместе со всеми связанными комментариями, полученные одним запросом GET.
Возможно, вы задаётесь вопросом, для чего нужны части ключей со значениями 0 и 1. Они нужны лишь для того, чтобы документ публикации всегда сортировался перед связанными с ней документами комментариев. Поэтому, получив результаты этого представления для конкретной публикации, вы будете знать, что первая строка содержит данные самой публикации в блоге, а остальные строки — данные комментариев.
Остаётся одна проблема: комментарии не упорядочены. Это лишь потому, что у нас нет связанной с ними информации о дате и времени. Если бы она у нас была, мы добавили бы временную метку третьим элементом массива ключа, вероятно, в виде строк даты и времени в формате ISO. Тогда мы по-прежнему использовали бы строку запроса ?startkey=["myslug"]&endkey=["myslug", 2]&include_docs=true для получения публикации в блоге и всех связанных комментариев, но теперь они были бы упорядочены по времени.
Copyright © 2025 The Apache Software Foundation — Licensed under the Apache License 2.0
https://docs.couchdb.org/en/3.5.1/ddocs/views/joins.html