(Cloudant) Создание представления для объединения двух типов документов

Допустим, я создаю базу данных Cloudant для хранения всех сервисных записей для моего парка автомобилей (нет, но проблема почти одинаковая). Для этого у меня есть два типа записей:

Машины:

  {
    "type": "Car",
    "_id": "VIN 1",
    "plateNumber": "ecto-1",
    "plateState": "NY",
    "make": "Cadillac",
    "model": "Professional Chassis",
    "year": 1959
  }

  {
    "type": "Car",
    "_id": "VIN 2",
    "plateNumber": "mntclmbr",
    "plateState": "VT",
    "make": "Jeep",
    "model": "Wrangler",
    "year": 2016
  }

И сервисные записи:

  {
    "type": "ServiceRecord",
    "_id": "service1",
    "carServiced": "VIN 1",
    "date": [1984, 6, 8],
    "item": "Cleaning (Goo)",
    "cost": 300
  }

  {
    "type": "ServiceRecord",
    "_id": "service2",
    "carServiced": "VIN 1",
    "date": [1984, 6, 9],
    "item": "Cleaning (Marshmellow)",
    "cost": 800
  }

  {
    "type": "ServiceRecord",
    "_id": "service3",
    "carServiced": "VIN 2",
    "date": [2016, 4, 2],
    "item": "Alignment",
    "cost": 150
  }

О том, как это работает, следует отметить несколько моментов:

  • Номер VIN автомобиля никогда не изменится, используется в качестве документа _id.
  • Сервисные записи для автомобиля не должны быть потеряны, если автомобиль зарегистрирован в новом состоянии или с новым номером.
  • Из-за большого количества автомобилей и того, как часто они нуждаются в ремонте, нецелесообразно редактировать автомобильный документ, если служебную запись необходимо добавить, удалить или изменить.

В настоящее время у меня есть пара просмотров для поиска информации.

Во-первых, у меня есть карта от номерного знака до VIN:

function(doc){
   if (doc.type == "Car"){
      emit([doc.plateState, doc.plateNumber], doc._id);
   }
}

// Results in:
["NY", "ecto-1"] -> "VIN 1"
["VT", "mntclmbr"] -> "VIN 2"

Во-вторых, у меня есть карта всех VIN-кодов автомобилей и служебных записей:

function(doc){
   if (doc.type == "ServiceRecord"){
      emit(doc.carServiced, doc);
   }
}

// Results in:
"VIN 1" -> {"_id": "service1", ...}
"VIN 1" -> {"_id": "service2", ...}
"VIN 2" -> {"_id": "service3", ...}

Наконец, у меня есть карта всех VIN-кодов и дат обслуживания для конкретной службы, которая произошла в эту дату:

function(doc){
   if (doc.type == "ServiceRecord"){
      var key = [doc.carServiced, doc.date[0], doc.date[3], doc.date[2]];
      emit(key, doc);
   }
}

// Results in:
["VIN 1", 1984, 6, 8] -> {"_id": "service1", ...}
["VIN 1", 1984, 6, 9] -> {"_id": "service2", ...}
["VIN 2", 2016, 4, 2] -> {"_id": "service3", ...}

С этими тремя картами я могу найти три разные вещи:

  • VIN любого автомобиля по номеру.
  • Сервисные записи любого автомобиля по его VIN.
  • Сервисные записи любого автомобиля по его VIN за любой конкретный год, месяц или день.

Тем не менее, не могу найти все сервисные записи автомобиля по его номеру. (По крайней мере, не за один шаг.) Для этого мне понадобится карта вроде этой:

["NY", "ecto-1"] -> {"_id": "service1", ...}
["NY", "ecto-1"] -> {"_id": "service2", ...}
["VT", "mntclmbr"] -> {"_id": "service3", ...}

И чтобы сделать это еще более сложным, я хотел бы иметь возможность просматривать служебные записи по номеру и дате, с картой, подобной этой:

["NY", "ecto-1", 1984, 6, 8] -> {"_id": "service1", ...}
["NY", "ecto-1", 1984, 6, 9] -> {"_id": "service2", ...}
["VT", "mntclmbr", 2016, 4, 2] -> {"_id": "service3", ...}

К сожалению, я не знаю, как создавать такие карты, потому что ключ требует информацию из двух документов. Я могу получить информацию о табличке только из документов Car, а информацию о сервисе (включая _id для значения emit) я могу получить только из документов ServiceRecord.

До сих пор я думал только о том, чтобы сделать два запроса: один для получения VIN из информации на табличке, а другой для получения служебных записей из VIN. Это будут быстрые запросы, так что это не большая проблема, но я чувствую, что есть лучший способ.

Кто-нибудь знает, что может быть лучше?

(Бонус: метод с двумя запросами не позволяет эффективно находить все сервисные записи по состоянию. Последняя карта, которую я опишу, сможет это сделать. Так что бонусные интернет-баллы для тех, кто может описать решение, которое обеспечивает это функциональность тоже.)

** Редактировать: другая проблема здесь была предложена в качестве возможного дубликата. Это определенно похожая проблема, однако предоставленные решения не решают эту проблему. В частности, верхнее решение предлагает сохранить положение документа в дереве. В этом случае это было бы что-то вроде "index":[State, Number, Year, Month, Day]" в документе ServiceRecord. Однако мы не можем этого сделать, потому что информация на табличке может легко измениться.

2 ответа

Решение

Надеюсь, ты все еще рядом. Суть ответа такова: в CouchDb, когда вы чувствуете необходимость делать объединения, вы в 99% случаев делаете что-то не так. Что вам нужно сделать, это иметь всю необходимую информацию в одном документе.

Вам нужно привыкнуть думать о том, как вы собираетесь запрашивать ваши данные, когда разрабатываете, что сохранять. Вы обнаружите, что замена привычки "нормализации отношений" на эту привычку полезна.

Здесь вы можете сохранить номерной знак в служебной документации. Не бойтесь денормализовать. Сервисная запись должна выглядеть следующим образом:

{
    "type": "ServiceRecord",
    "_id": "service3",
    "carServiced": "VIN 2",
    "carPlateNumber": "mntclmbr", 
    "date": [2016, 4, 2],
    "item": "Alignment",
    "cost": 150
}

И вы можете легко сделать все, что вы хотите отсюда. При этом, архитектор, которым я являюсь, может чувствовать запах, что вы, вероятно, изобрели новые способы запрашивать эти данные каждый месяц. По этой причине я лично предпочел бы хранить весь автомобильный документ в служебной записи:

{
    "type": "ServiceRecord",
    "_id": "service3",
    "carServiced":  {
        "type": "Car",
        "_id": "VIN 2",
        "plateNumber": "mntclmbr",
        "plateState": "VT",
        "make": "Jeep",
        "model": "Wrangler",
        "year": 2016
      }, 
   "date": [2016, 4, 2],
        "item": "Alignment",
        "cost": 150
}

Это абсолютно нормально. Тем более что служебная запись является моментальным снимком и вам не нужно беспокоиться об обновлении информации. На самом деле я обнаружил, что это один из сценариев, в котором CouchDb особенно хорош, поскольку хранение снимка в основном является бесплатным обедом (в отличие от управления таблицей cars_snapshot в реляционной системе). И мы склонны забывать об этом, но очень часто (особенно в том, что касается продаж), мы заинтересованы в снимках, а не в актуальных реляционных данных (как назывался клиент на момент покупки, какова была налоговая ставка на тот момент он купил и т. д.). Но реляционные системы ставят нас в привычку "обновлять по умолчанию", потому что управление моментальными снимками требует значительных затрат.

Суть в том, что этот вид денормализации абсолютно подходит в CouchDb. Вы находитесь в предполагаемом использовании и не будете укушены сзади по дороге. Как говорит CouchDb: просто расслабься;)

Звучит так, будто цепочка mapreduce может дать вам решение? https://examples.cloudant.com/sales/_design/sales/index.html

Другие вопросы по тегам