В этой статье я расскажу вам о моделировании Social Library, социального приложения для книжных энтузиастов, с помощью GunDB. Это вымышленное приложение помогает читателям создавать списки избранных из понравившихся им книг, оставлять отзывы и подписываться на других читателей и авторов. Я начну с разговора о том, что такое моделирование данных, а затем расскажу, как создать графовую модель данных для приложения. Затем я реализую сущности и отношения с GunDB, и, наконец, я создам несколько поддельных данных и покажу вам, как выполнять некоторые запросы к данным. Если вы просто хотите прочитать часть, относящуюся к GunDB, вы можете перейти к разделу Проектирование для GunDB. Если вы новичок в GunDB, возможно, вы захотите прочитать мою другую статью по основам GunDB.

Все фрагменты кода для этой статьи доступны на Gitlab.

Заявление об ограничении ответственности

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

Вступление

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

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

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

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

Теперь для графовых баз данных, независимо от базы данных, вы почти всегда выбираете следующие три компонента:

  • Узлы: сущности системы
  • Свойства узла: атрибуты каждой сущности
  • Ребра: отношения между сущностями

Моделирование приложения социальной библиотеки

В следующих разделах мы рассмотрим процесс разработки приложения Social Library. Мы определим некоторые объекты, их атрибуты и отношения. Мы расширим дизайн GunDB и, наконец, создадим фальшивые данные и исследуем выполняющиеся запросы. Обратите внимание, что некоторые функции приложения могут быть не включены в модель данных. Но мы рассмотрим достаточно сущностей и отношений, чтобы у вас была хорошая отправная точка, если вы были заинтересованы в разработке модели для всего приложения.

Случаи применения

Ниже приведены варианты использования, которые мы определили после сбора информации о приложении:

Пользователи

  • Пользователь может быть читателем, автором, издателем или сочетанием всех трех.
  • В приложении будут читатели и авторы, которые смогут создавать аккаунты.
  • Читатели и авторы могут иметь профили, которые они могут использовать, чтобы немного рассказать о себе.
  • Администраторы могут выполнять административные задачи, такие как создание книг или управление издателями в системе.
  • Издатели могут управлять изданными ими книгами

Читатели

  • Читатели могут любить книги. Они могут создавать списки и добавлять книги в свои списки. Книга может быть в нескольких списках в зависимости от того, что выберет читатель. Они также могут решить, какой список должен оставаться частным, а какой - открытым.
  • Читатели могут просматривать книги. Они могут поставить книгу от 1 до 5 и оставить отзыв о ней. Они могут отправлять обзоры и отображать их на странице сведений о книге.
  • Читатели могут следить за другими читателями. Вы можете перейти в профиль читателя и увидеть его подписчиков, а также читателей, на которых они подписаны.
  • Читатели также могут следить за авторами. Как и в примере выше, вы можете видеть авторов, за которыми они следят, и авторов, которые следят за ними.

Канал

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

Книги

  • Администраторы могут добавлять книги и управлять существующими.
  • Книги должны быть легко доступны для поиска по категориям или ключевым словам.
  • Книги следует добавлять таким образом, чтобы читателям можно было давать рекомендации в зависимости от того, какие книги они любили или читали.

Идентификация сущностей, отношений и запросов

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

Сущности

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

  • Пользователь
  • Читатель
  • Автор
  • Издатель
  • Книга
  • Категория книги

Атрибуты

Вот атрибуты упомянутых выше сущностей:

Пользователь

  • имя
  • Эл. адрес
  • имя пользователя
  • роли

Читатель:

  • имя
  • любимые книги
  • отзывы
  • следующий
  • подписчики

Автор:

  • имя
  • книги
  • подписчики
  • следующий

Книга:

  • заглавие
  • подзаголовок?
  • isbn
  • авторы
  • издатель
  • категории
  • обзоры?

Категория книги:

  • имя

Отношения

Ниже приведены некоторые отношения между упомянутыми выше объектами:

  • Пользователь может быть читателем, автором, издателем или сочетанием трех ролей.
  • Пользователи могут подписаться на 0 для многих пользователей
  • Читатели могут просмотреть от 0 до многих книг
  • Читатели могут добавить в избранное от 0 до многих книг
  • Книги относятся к одной или нескольким категориям
  • Авторы могут писать от 0 до многих книг
  • Издатели издают от 0 до многих книг

На приведенном ниже рисунке показаны вышеупомянутые отношения:

Выявление запросов

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

Для читателя:

  • получить их отзывы
  • получить своих подписчиков (авторов или других читателей)
  • узнай, за кем они следят
  • получить их частные и общедоступные списки избранных книг

Автор:

  • получить книги, которые они написали
  • получить своих последователей (читателей или других авторов)
  • узнай, за кем они следят

Дана книга:

  • получить категории, к которым он принадлежит
  • получить его отзывы
  • получить его авторов
  • получить своего издателя
  • получить читателей, которые добавили это в избранное
  • получить авторов, которые являются авторами или соавторами

Для издателя

  • получить книги, которые они опубликовали
  • получить авторов, которые работали с издателем

Учитывая категорию книги:

  • получить книги, которые относятся к категории
  • учитывая книгу, получите все категории, к которым она принадлежит

В дополнение к вышеуказанным запросам мы также хотим иметь возможность ответить на следующие вопросы:

  • Какие книги самые популярные? Популярную книгу можно определить как книгу, у которой больше всего 5-звездочных обзоров и наибольшее количество читателей, добавляющих их в свои любимые списки.
  • Какие заголовки, ключевые слова или категории искали чаще всего?
  • Какие книги с этим рейтингом имеют такой рейтинг? Например, мы хотим видеть все книги с рейтингом 2.
  • Какие книги нравятся двум или более читателям? Это предполагает только общедоступные списки избранного, которыми пользуются читатели.
  • Учитывая двух или более читателей / авторов, кто у них общих подписчиков / подписчиков?
  • По заданному ключевому слову вернуть все книги, в которых есть ключевое слово для поиска.

Проектирование для GunDB

В этом разделе мы собираемся использовать информацию из предыдущих разделов и создать модель графа, которая будет специально реализована для GunDB.

Узлы и свойства

Сначала давайте поговорим об узлах, представляющих сущности, и определим их свойства:

Книга

book
- uuid: string (internal)
- type: string (internal)
- title: string
- subtitle: string
- isbn: string
--
* reviews: Set
* categories: Set
* authors: Set
* publisher: Link

Совет: если вы используете draw.io для создания диаграмм, вы можете автоматически создать диаграмму из определения, приведенного выше. См. Приложение 1 для более подробной информации.

Категория книги

book_category
- uuid: string (internal)
- type: string (internal)
- name: string
--
* books: Set

Я использую звездочку *, чтобы указать свойство, которое является указателем или ссылкой на другой узел или набор

Пользователь

user
- uuid: string (internal)
- type: string (internal)
- name: string
- username: string
- email: string
--
* roles: Set

Читатель

reader
- uuid: string (internal)
- type: string (internal)
- name: string
--
* book_reviews: Set
* following: Set
* followers: Set
* favorite_books: Set

Автор

author
- uuid: string (internal)
- type: string (internal)
- name: string
--
* books: Set
* following: Set
* followers: Set

Издатель

publisher
- uuid: string (internal)
- type: string (internal)
- name: string
- address: string
--
* books: Set
* authors: Set

На рисунке ниже приведены все описания узлов, определенные выше:

Отношения

В этом разделе мы собираемся изучить представление отношений с GunDB. GunDB по умолчанию создает отношения в одном направлении и не заставляет вас определять свойства для ребер. Это дает вам свободу решать, для каких ребер требуются свойства, а для каких объектов - двунаправленные отношения. У вас есть полный контроль над созданием графической модели вашей системы. Однако полезно использовать узлы ссылок для описания свойств отношений.

Рецензия:

Рецензирование книги может быть представлено узлом связи между читателем и книгой со следующими свойствами:

review_book (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- rating: number (integer, 1 <= n < 6)
- content: string (max 375 characters)
--
* book: Node
* reader: Node

Этот узел ссылки имеет некоторые свойства, включая значение рейтинга и содержание обзора. Он также включает две ссылки: одну на книгу, а другую - на читателя. Ниже приведена диаграмма, описывающая, как «экземпляр» review_book будет выглядеть по отношению к книге и читателю:

О приведенной выше диаграмме стоит упомянуть несколько моментов:

  • Мы специально сделали все ссылки двунаправленными, чтобы мы могли перемещаться по ссылкам с любого заданного узла.
  • Из узла ссылок review_book мы можем перейти к читателю или к книге.
  • Из reader мы можем перейти к их обзорам из book_reviews набора
  • Из book мы можем перейти к обзорам из reviews набора

Мы также можем описать вышеупомянутые отношения в виде простого текста:

;Review Book:
reader->book_reviews->book_reviews(set)
book_reviews(set)->review_book
review_book->reader->reader
review_book->book->book
book->reviews->reviews(set)
reviews(set)->review_book

Совет: вы можете использовать Draw.io для автоматического создания диаграммы из определения графика выше. См. Приложение 1 для более подробной информации.

Остальные отношения, которые мы собираемся исследовать, очень похожи по структуре на рассмотренные выше. То есть начальный или конечный узлы будут иметь набор, указывающий на связи отношений. И сама ссылка на узел будет иметь одну ссылку на «источник», а другую - на узел «назначения».

Авторская книга

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

author_book (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- date: string (ISO date)
--
* book: Node
* author: Node

Отношения между автором и книгами показаны ниже:

;Author Book:
author->books->books(set)
books(set)->author_book
author_book->author->author
author_book->book->book
book->authors->authors(set)
authors(set)->author_book

Любимые книги

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

favorite_list (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- list_name: string
- is_public: string ("true" or "false")
--
* books: Set
* belongs_to: Node

Узлы и отношения показаны на схеме ниже:

;Reader's favorite books:
reader->favorite_books->favorite_books(set)
favorite_books(set)->book_list
book_list->books->books(set)
book_list->belongs_to->reader
books(set)->book

Категория книги

Связь категорий книги может быть представлена ​​узлом связи между книгой и узлом категории:

book_category (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- category_name: string
--
* book: Node
* belongs_to: Node

Узлы и отношения показаны на схеме ниже:

;Books in a category:
category->books->books(set)
books(set)->book_category
book_category->belongs_to->category
book_category->book->book
book->categories->categories(set)
categories(set)->book_category

Опубликовать книгу

Публикация книги может быть представлена ​​узлом связи между издателем и книгой:

publish_book (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- date: string (ISO date)
--
* book: Node
* publisher: Node

Отношения между книгой и ее издателем показаны ниже:

;Publish Book:
publisher->books->books(set)
books(set)->publish_book
publish_book->publisher->publisher
publish_book->book->book
book->publication_details->publish_book

Роли пользователей

Роль пользователя может быть представлена ​​узлом связи между узлом пользователя и типом пользователя, например читателем, автором или издателем:

role (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- role_name: string
--
* role_type: Node
* user: Node
* permissions: Set

На диаграмме ниже показано, как будут выглядеть отношения:

;User Roles:
user->roles->roles(set)
roles(set)->role
role->assigned_to->user_type
role->user->user
user_type->role->role
roles/name->role

Подпишитесь на читателей / авторов

Следующие за читателем или автором могут быть представлены узлом связи между ними:

follow (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- date: string (ISO date)
--
* reader: Node
* by_reader: Node

На приведенной ниже диаграмме описываются отношения между одним читателем, следующим за другим:

Обратите внимание, что аналогичные отношения могут быть определены для читателей, следующих за авторами, или авторов, следующих за читателями, или другими авторами. Вы по-прежнему можете оставить имя отношения как «следовать», но тогда вы, вероятно, захотите переименовать свойства ссылки на более подходящие имена. Например, отношения между автором и читателем можно определить как:

follow (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- date: string (ISO date)
--
* reader: Node
* by_author: Node

Другая возможность - обобщить отношения «подписки» от одного пользователя к другому и не беспокоиться о типе пользователя. В этом случае отношения можно определить как:

follow (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- date: string (ISO date)
--
* user: Node
* by_user: Node

В этом случае, пока пользователи и типы пользователей связаны должным образом, вы можете перемещаться по графу из любого узла и находить их последователей и тех, за кем они следят. Мы рассмотрим это подробнее при создании графика с помощью GunDB в следующем разделе.

Создание графика

Теперь, когда мы определили узлы, их свойства и отношения, пора добавить поддельные данные и протестировать модель. Цель здесь - создать график с помощью GunDB, используя приведенные выше определения, и выполнить несколько запросов к данным, чтобы убедиться, что мы получили обратно правильные данные.

Добавление поддельных данных

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

function fakeBooks(opts = {n: 5}) {

  const books = [];
  let howMany = opts.n;

  while(howMany-- > 0) {
    let id = uuid();
    let count = (opts.n - howMany);
    const book = {
      uuid: id,
      type: "Book",
      title: `The Book Title ${count}`,
      subtitle: `Lorem ipsum dolor ${count}`,
      isbn: count,
    };
    books.push(book);
  }

  return books;
}

Вышеупомянутая функция по умолчанию возвращает массив из пяти простых объектов JavaScript, представляющих книги. Затем мы хотим вызвать эту функцию и создать узлы Gun из объектов книги:

const db = Gun();
const books = fakeBooks();
for (let b of books) {
  db.get(b.uuid).put(b);
}

Для удобства мы также собираемся создать массив JavaScript, который будет содержать ссылки на только что созданные узлы Gun:

const bookNodes = books.map(b => db.get(b.uuid));

Мы можем повторить те же шаги для создания поддельных данных для других сущностей. Остальные функции для генерации поддельных данных все включены в репозиторий статей на Gitlab.

Добавление отношений

Обзорная книга

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

  • Узел ссылки review_book представляет читателя, просматривающего книгу.
  • Свойство book в review_book ссылается на рецензируемую книгу.
  • Свойство reader в reivew_book ссылается на читателя, создающего обзор.
  • Набор reviews/r содержит ссылки на review_book узлов, сгруппированных по рейтингу.
  • Свойство book_reviews в узле reader - это набор, содержащий ссылки на review_book узлы.
  • Свойство reviews в узле book - это набор, содержащий ссылки на review_book узлы.

Теперь мы можем определить функцию, которая создает узлы и ссылки, показанные выше:

function reviewBook(opt) {
  const {db, reader, book, rating, content} = opt;
  const linkId = uuid();

  const review = db.get(linkId).put({ // A
    uuid: linkId,
    type: "Link",
    name: "review_book",
    rating: rating,
    content: content,
  });
  review.get("book").put(book); // B
  review.get("reader").put(reader); // C

  db.get(`reviews/${rating}`).set(review); // D

  book.get("reviews").set(review); // E
  reader.get("book_reviews").set(review); // F

  return review;
}

В приведенном выше фрагменте функция reviewBook принимает объект, содержащий следующие ссылки:

  • экземпляр db
  • узел чтения
  • книжный узел
  • значение оценки для обзора
  • контент для обзора

Затем мы создаем узел ссылки review_book в строке A, устанавливая рейтинг и контент, используя переданные значения. В строке B мы создаем свойство обзора с именем book, которое указывает на данный узел книги. В строке C мы определяем свойство с именем reader, которое указывает на данный узел чтения. В строке D мы добавляем узел обзора в набор review/rating. Этот набор поможет нам ссылаться на обзоры по рейтинговым значениям. В строке E мы создаем набор на данном узле книги с именем reviews и добавляем к нему узел обзора. В строке F мы создаем набор на узле чтения с именем book_reviews и добавляем к нему узел обзора. И, наконец, мы возвращаем узел ссылки review_book из функции.

Теперь, когда у нас есть эта функция, в основном файле мы можем создавать отзывы читателей. Например, мы можем создать два обзора Читателем 1:

reviewBook({
  db,
  reader: readerNodes[0],
  book: bookNodes[0],
  rating: 5,
  content: "Great book!",
});

reviewBook({
  db,
  reader: readerNodes[0],
  book: bookNodes[1],
  rating: 1,
  content: "It was ok.",
});

Чтобы быстро проверить взаимосвязь рецензий на книги, мы можем выполнить следующие запросы и вручную проверить результаты:

  • Записывать все отзывы Reader 1
readerNodes[0].get("book_reviews").map().once(console.log);
  • Запишите названия всех книг, просмотренных Читателем 1:
readerNodes[0].get("book_reviews").map().get("book").get("title").once(console.log);
  • Учитывая Книгу 1, запишите ее рецензии:
bookNodes[0].get("reviews").map().once(console.log);
  • Учитывая Книгу 1, запишите 5-звездочные обзоры:
bookNodes[0].get("reviews")
.map()
.once(review => {
  if(review.rating === 5) {
    db.get(review.book).once(b => {
      console.log(review);
    });
  }
});
  • Учитывая Книгу 1, запишите имена читателей, которые рецензировали книгу:
bookNodes[0].get("reviews").map().get("reader").get("name").once(console.log);
  • Запишите все обзоры с одной звездой:
db.get("reviews/1").map().once(console.log);
  • Учитывая обзоры с одной звездой, запишите названия книг:
db.get("reviews/1").map().get("book").get("title").once(console.log);
  • Учитывая отзывы с одной звездой, запишите читателей, оставивших отзывы:
db.get("reviews/1").map().get("reader").get("name").once(console.log);

Все вышеперечисленные запросы выполняются нормально, но я заметил повторяющиеся результаты (когда количество записей увеличивается) после выполнения запросов, подобных приведенному ниже:

readerNodes[0].get("book_reviews").map().get("book").once(log);

И вместо этого я делаю следующее:

readerNodes[0].get("book_reviews").map().once(v => db.get(v.book).once(log));

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

readerNodes[0].get("book_reviews").map().get("book").get("title").once(console.log);

В приведенном выше фрагменте вместо того, чтобы регистрировать узлы книг, я регистрирую названия книг, и это прекрасно работает.

Запросы с использованием обещаний

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

Во-первых, нам нужно включить расширение then. Если вы используете npm для установки Gun, расширение then будет включено в node_modules/gun/lib/then. Вы можете загрузить его после включения Gun:

const gun = require("gun");
require("gun/lib/then");

После включения расширения цепочка Gun включает метод then, который можно использовать для превращения цепочки в обещание. Например, следующий фрагмент кода возвращает обещание, которое преобразуется в карту, содержащую ключи и метаданные для каждого ключа:

const result = readerNodes[0].get("book_reviews").then();

Теперь, чтобы извлечь данные, вы можете сделать что-то вроде этого:

const removeMetaData = (o) => { // A
  const copy = {...o};
  delete copy._;
  return copy;
};

const bookReviews = readerNodes[0].get("book_reviews").then() // B
.then(o => removeMetaData(o)) // C
.then(refs => Promise.all(Object.keys(refs).map(k => db.get(k).then()))) // D
.then(r => console.log(r)); // E
  • В строке A мы определяем вспомогательную функцию для создания копии объекта и удаления поля метаданных «_».
  • В строке B мы запускаем запрос на получение обзоров книг, созданных читателем, и запускаем цепочку обещаний.
  • В строке C мы используем нашу вспомогательную функцию для удаления метаданных и возврата копии, которая включает только ключи результатов.
  • В строке D мы берем ссылки и используем db.get, чтобы преобразовать их в узлы данных. Вот разбивка каждого шага для линии D:

Строка D

  • Object.keys(refs): возвращает массив JavaScript, содержащий ключи / ссылки Gun
  • .map(k => db.get(k).then()): возвращает массив JavaScript, содержащий результат db.get.then для каждого ключа. Результат - массив обещаний.
  • Promise.all принимает массив обещаний и распределяет их по узлам.
  • И, наконец, в строке E мы регистрируем узлы данных.

Вот еще один запрос с использованием обещаний:

  • Зарегистрируйте все книги с рейтингом одной звездочки:
db.get("reviews/1").then()
.then(filterMetadata)
.then(r => Promise.all(Object.keys(r).map(k => db.get(k).then())))
.then(r => Promise.all(Object.keys(r).map(k => db.get(r[k].book["#"]).then())))
.then(r => log(r));

В репозитории статьи есть помощник по запросам, который просто отвечает на запросы, указанные выше. Вы можете увидеть несколько примеров в файле query_test.js.

Авторская книга

Следующие отношения, которые мы рассмотрим, - это создание книг. На схеме ниже показаны ссылки и узлы, которые мы собираемся создать:

  • Узел ссылки author_book представляет собой создание книги одним или несколькими авторами.
  • Свойство book в узле author_book ссылается на авторскую книгу.
  • Свойство author в узле author_book ссылается на автора книги.
  • Свойство book в узле author ссылается на набор, содержащий ссылки на узлы author_book.
  • Свойство authors в узле book ссылается на набор, содержащий ссылки на узлы author_book.

Теперь мы можем определить функцию, которая создает узлы и ссылки, показанные выше:

function authorBook(opt) {
  const {db, author, book, date} = opt;
  const linkId = uuid();

  const authorBookNode = db.get(linkId).put({
    uuid: linkId,
    type: "Link",
    name: "author_book",
    date: date,
  });
  authorBookNode.get("book").put(book);
  authorBookNode.get("author").put(author);

  book.get("authors").set(authorBookNode);
  author.get("books").set(authorBookNode);

  return authorBookNode;
}

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

  • Запишите названия книг, созданных Автором 1:
authorNodes[0].get("books").map().get("book").get("title").once(log);
  • Запишите имена авторов Книги 1:
bookNodes[0].get("authors").map().get("author").get("name").once(log);

Мы также можем комбинировать запросы из «рецензии на книгу» и возвращать результаты рецензии, просматривая график от автора. Например:

  • Если указан автор, зарегистрируйте названия книги, которую он написал, и все оценки, связанные с книгами:
authorNodes[0].get("books").map().get("book").get("title").once(log);
authorNodes[0].get("books").map().get("book").get("reviews").map().get("rating").once(log);

Любимые книги

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

  • Узел book_list представляет собой список, содержащий набор книг, добавленных читателем.
  • Свойство books в book_list содержит ссылку на набор книг.
  • Свойство belongs_to ссылается на читателя, создавшего список.
  • Свойство favorite_books в узле reader ссылается на набор, содержащий все ссылки на узел book_list.

Теперь мы можем определить функцию, которая создает узлы и ссылки, показанные выше:

function favoriteBooks(opt) {
  const {db, reader, books, listName} = opt;
  const listId = uuid();

  const list = db.get(listId).put({
    uuid: listId,
    type: "Link",
    name: "favorite_list",
    list_name: listName,
  });

  const faveBooks = db.get(uuid());
  for (book of books) {
    faveBooks.set(book);
  }

  list.get("books").put(faveBooks);
  list.get("belongs_to").put(reader);

  reader.get("favorite_books").set(list);

  return list;
}

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

  • Записывайте все любимые книги для Reader 1 во все их списки
readerNodes[0].get("favorite_books").map().get("books").map().get("title").once(log);
  • Зарегистрируйте названия любимых книг для Читателя 1 в списке «Список 1».
readerNodes[0].get("favorite_books").map().once(list => {
  if(list.list_name === "List 1") {
    db.get(list.books).map().get("title").once(log);
  }
});
  • Запишите имена частных списков, созданных Reader 1
readerNodes[0].get("favorite_books").map().once(list => {
  if(list.is_public === "false") {
    log(list.list_name);
  }
});

Публиковать книги

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

  • Узел publish_book - это узел ссылки, который содержит ссылки на опубликованную книгу и издателя.
  • Свойство book в узле publish_book ссылается на опубликованную книгу.
  • Свойство publisher в узле publish_book ссылается на издателя.
  • Узел книги содержит свойство под названием publication_details, которое также ссылается на узел publish_book.
  • У издателя есть свойство под названием books, которое представляет собой набор, содержащий ссылки на узлы ссылок publish_book.

Используя информацию выше, мы можем создать функцию, которая создает узлы и отношения:

function publishBook(opt) {
  const {db, publisher, book, date} = opt;
  const linkId = uuid();

  const publishLink = db.get(linkId).put({
    uuid: linkId,
    type: "Link",
    name: "publish_book",
    date: date,
  });
  publishLink.get("book").put(book);
  publishLink.get("publisher").put(publisher);
  book.get("publication_details").put(publishLink);
  publisher.get("books").set(publishLink);

  return publishLink;
}

После создания узлов и отношений мы можем выполнить следующие запросы:

  • Зарегистрируйте названия книг, опубликованных Издателем 1
publisherNodes[0].get("books").map().get("book").get("title").once(log);
  • Зарегистрируйте названия книг, опубликованных Издателем 2.
publisherNodes[1].get("books").map().get("book").get("title").once(log);
  • Для данной книги 1 укажите имя ее издателя.
bookNodes[0].get("publication_details").get("publisher").get("name").once(log);

Категория книги

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

  • Узел ссылки book_category связывает категорию с книгой.
  • Свойство belongs_to в узле book_category ссылается на категорию.
  • Свойство book в узле book_category ссылается на книгу.
  • Набор category/name содержит ссылки на book_category узлы.
  • Свойство books в узле category - это набор, содержащий ссылки на узлы ссылок book_category.
  • Свойство categories в узле book - это набор, который также содержит ссылки на узлы ссылок book_category.

Используя информацию выше, мы можем создать функцию, которая создает узлы и ссылки для категоризации книги:

function bookCategory(opt) {
  const {db, category, book, categoryName} = opt;
  const linkId = uuid();

  const categoryLink = db.get(linkId).put({
    uuid: linkId,
    type: "Link",
    name: "book_category",
    category_name: categoryName,
  });

  categoryLink.get("book").put(book);
  categoryLink.get("belongs_to").put(category);
  db.get(`category/${categoryName}`).set(categoryLink);

  book.get("categories").set(categoryLink);
  category.get("books").set(categoryLink);

  return categoryLink;
}

После создания узлов и отношений мы можем выполнить следующие запросы:

  • Зарегистрируйте названия всех книг в Категории 1:
categoryNodes[0].get("books").map().get("book").get("title").once(log);
  • Для данной книги запишите названия категорий, к которым она принадлежит:
bookNodes[0].get("categories").map().get("belongs_to").get("name").once(log);
  • Учитывая категории, зарегистрируйте названия книги в Категории 1:
db.get("category/Category 1").map().get("book").get("title").once(log);

Роли пользователей

Чтобы назначить роли пользователю, мы можем создать набор и добавить в набор ссылки ролей. На схеме ниже показаны узлы и ссылки, которые нам нужно создать:

  • Ссылка на узел role содержит информацию о роли. Его свойство user ссылается на пользовательский узел, а его свойство assigned_to ссылается на узел пользовательского типа. Узел типа пользователя может быть читателем, автором или издателем.
  • Свойство roles в узле user - это набор, содержащий ссылки на role узлы.
  • Узел типа пользователя имеет свойство role, которое указывает на узел role.

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

  • users/uuid: набор, содержащий ссылки на созданных пользователей.
  • roles/name: набор, содержащий ссылки на созданные роли.
  • [user_types]: набор, содержащий ссылки на созданные типы пользователей. Например readers, authors, publishers.

Используя информацию выше, мы можем определить функцию, которая создает узлы и ссылки:

function userRole(opt) {
  const {db, name, userTypeNode, user} = opt;
  const linkId = uuid();

  const roleLink = db.get(linkId).put({
    uuid: linkId,
    type: "Link",
    name: "role",
    role_name: name,
  });

  roleLink.get("user").put(user);
  roleLink.get("assigned_to").put(userTypeNode);
  db.get(`roles/${name}`).set(roleLink);
  const userUuid = user._.put.uuid; // HACK, DONT DO THIS IN ACTUAL APP
  const userType = userTypeNode._.put.type.toLowerCase() + "s"; // HACK, DONT DO THIS IN ACTUAL APP
  db.get(`users/${userUuid}`).set(user);
  db.get('users').set(user);
  db.get(userType).set((userTypeNode));

  user.get("roles").set(roleLink);
  userTypeNode.get("role").put(roleLink);

  return roleLink;
}

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

После создания узлов и отношений мы можем выполнить следующие запросы:

  • Запишите имена всех читателей
db.get("readers").map().get("name").once(log);
  • Регистрируйте электронные письма всех пользователей
db.get("users").map().get("email").once(log);
  • Зарегистрируйте роли пользователя 1:
userNodes[0].get("roles").map().get("role_name").once(log);

Следуйте за людьми

Отношения «следовать» немного отличаются от отношений, которые мы рассмотрели до сих пор. Это потому, что большинство вышеперечисленных отношений, таких как создание книги или рецензирование книги, подразумевают двусторонние отношения. Но следование не обязательно двунаправленное. Например, пользователь A может подписаться на пользователя B, но это не означает, что пользователь B также сразу же следует за пользователем A. Из-за этого нам нужно будет определить два набора для каждого пользователя, чтобы отслеживать «подписчиков» и «подписчиков». На диаграмме ниже показаны узлы и ссылки, которые нам нужно создать, когда пользователь A следует за пользователем B:

  • Узел ссылки follow представляет собой связь «следовать» между userA и userB.
  • Свойство who в узле ссылки follow представляет «пункт назначения» отношения «следовать».
  • Свойство by в узле ссылки follow представляет «источник» отношения «следовать».
  • Свойство following в userA - это набор, содержащий ссылки на follow узлы.
  • Свойство followers в userB - это набор, содержащий ссылки на узлы follow.

Теперь, если UserB также следует за UserA, у нас будут следующие отношения:

Теперь, используя приведенную выше информацию, мы можем определить функцию, которая может связать двух людей с учетом исходного узла (by) и конечного узла (who):

function follow(opt) {
  const {db, sourceNode, destinationNode} = opt;
  const linkId = uuid();

  const followLink = db.get(linkId).put({
    uuid: linkId,
    type: "Link",
    name: "follow",
    date: new Date().toISOString(),
  });
  followLink.get("who").put(destinationNode);
  followLink.get("by").put(sourceNode);

  sourceNode.get("following").set(followLink);
  destinationNode.get("followers").set(followLink);

  return followLink;
}

После создания узлов и отношений мы можем выполнить следующие запросы:

  • Запишите имена всех людей, за которыми следует Читатель 2
readerNodes[1].get("following").map().get("who").get("name").once(log);
  • Запишите имена всех людей, которые подписаны на Reader 2.
readerNodes[1].get("followers").map().get("by").get("name").once(log);

Целостные запросы

Теперь, когда мы создали некоторые данные и добавили отношения, давайте зададим следующий комплексный вопрос:

  • Какая книга самая популярная?

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

// uuids of the books included in all favorite lists
q(db).get("readers").getSet()
.get("favorite_books").getSet()
.get("books").getSet().get("uuid")
.data();

// uuids of the 5-star books
q(db).get("reviews/5").getSet()
.get("book")
.get("uuid")
.data();

Затем мы можем подсчитать количество книг, которые были включены в списки избранного, и отсортировать их в порядке убывания. Первой книгой будет книга с наибольшим числом включений. Затем мы можем просто проверить, включена ли эта книга в 5-звездочный список. Вы можете увидеть полный фрагмент кода в файле main.js.

Вы можете использовать приведенный выше пример для выполнения всех видов запросов, чтобы получить интересную информацию о ваших данных. Идея состоит в том, чтобы сначала запустить несколько запросов для сбора интересующих uuid. Как только они у вас появятся, остается лишь провести базовое сравнение, чтобы найти ответ, который вы ищете.

Дальнейшие улучшения

Есть несколько вещей, которые вам следует учесть, чтобы улучшить реализацию:

  • Использование проверки схемы для обеспечения соблюдения структуры перед сохранением данных. Существует расширение под названием gun-schema, которое за кулисами использует пакет is-my-json-valid, чтобы помочь вам проверить форму ваших объектов.
  • Создание областей для ваших данных. Существует расширение под названием Reticle, которое помогает создавать области, чтобы избежать конфликтов между клавишами.
  • Проверка ограничений перед добавлением в базу данных важна, если вы хотите убедиться, что не добавляются неожиданные данные. Кроме того, вы также можете проверить уникальность, если этого требуют отношения.
  • Использование GraphQL - это один из способов установления протокола данных между клиентами и серверной частью системы. Используя GraphQL, клиенты могут спрашивать, что им нужно, и не беспокоиться о деталях серверной части. Вы можете посмотреть пакет Graphql-gun, который представляет собой graphql API для GunDB.

И, пожалуйста, присоединяйтесь к чату GunDB, если у вас есть вопросы или вы в чем-то не уверены, все очень дружелюбны и готовы помочь.

Заключение

Я надеюсь, что эта статья дала вам несколько идей о том, как приступить к моделированию данных с помощью GunDB. В этой статье я лишь поверхностно коснулся того, что возможно с GunDB. Это ни в коем случае не исчерпывающая модель всей системы. Моделирование данных, как и любой процесс проектирования, требует тщательного планирования и экспериментов. Однако хороший и хорошо продуманный дизайн может сэкономить вам много времени и усилий в будущем. Если у вас есть какие-либо мысли о том, как улучшить модели, представленные в этой статье, оставьте комментарий, мы будем благодарны за любой вклад.

Приложение 1: Draw.io

Мой любимый инструмент для построения диаграмм - Draw.io, доступный по адресу https://draw.io. В нем есть все функции, которые можно ожидать от инструмента для построения диаграмм.

Я собираюсь показать вам мою любимую функцию Draw.io. Эта функция позволяет автоматически преобразовывать описания в диаграммы. Это моя любимая функция, потому что я могу описывать модели и отношения простым текстом и очень быстро их визуализировать. Кроме того, я могу отслеживать изменения текста в Git с течением времени и видеть, что изменилось. В противном случае я бы не смог этого сделать, если бы все документировали только PDF-файлы или изображения.

Вы можете определить узел и его свойства, используя следующий формат:

node name
- property
- property2
--
* ref
* ref 2

Затем вы можете создать диаграмму из приведенного выше описания, выбрав Упорядочить ›Вставить› Из текста. Откроется небольшое окно. Внизу выберите «Список» из раскрывающегося списка, скопируйте и вставьте приведенный выше пример и нажмите «Вставить». Результат вы можете увидеть на рисунке ниже:

Вы можете использовать тот же метод для создания графиков. Но вместо того, чтобы выбирать «Список» из раскрывающегося списка, вам нужно будет выбрать вариант «Диаграмма». Ниже приведен простой пример, описывающий формат, который принимает Draw.io:

;GraphName:
a->b
b->edge label->c
c->a

Приведенный выше фрагмент создает следующую диаграмму: