Необходимое условие: в этой статье предполагается, что у вас есть базовые знания о приложениях и базах данных node.js (как реляционных, так и нереляционных).

В рабочих примерах в качестве ODM используется mongodb и библиотека mongoose.

Часть 1: Использование нормализации

Часть 2: Использование встроенных документов

Часть 3: Использование гибридного подхода

Существует три метода хранения данных в нереляционных базах данных, а именно использование нормализации, использование встроенных документов и использование гибридного подхода — сочетание первых двух методов.

То, как вы выбираете сохранение данных, влияет на то, на какую сторону уравнения производительности запроса согласованности данных V вы опираетесь.

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

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

Чтобы обеспечить согласованность данных, были созданы такие концепции, как транзакции базы данных, нормализация и т. д., для управления и организации связанных данных в базах данных.

В этой статье мы обсудим понятие нормализации.

Нормализация просто означает, что данные не дублируются в вашей базе данных.

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

Это устраняет дублирование данных и проблемы с обслуживанием, поскольку обновления данных выполняются только в одном месте.

В реляционных БД в одном SQL-запросе можно join данные из разных таблиц поэтому нормализация (разделение данных) несомненно хорошо для реляционных БД, однако с нереляционными БД все не так просто.

Нормализация может быть не так хороша для нереляционных баз данных.

Возможно, это не самая популярная фраза в 2022 году, но не убивайте мессенджера 😆.

Настройка проекта.

Вы можете получить доступ ко всем примерам кода из этого репозитория. Файлы сохраняются в каталогах (встроенные, нормализация и гибридные). У каждого есть отдельный файл data.json и index.js.

Настройте проект node.js и установите пакет mongoose. Этот пакет упрощает взаимодействие с mongoDB.

В этом примере в качестве библиотеки ODM используется mongoose версии 6.1.8

Проверьте, есть ли у вас локальная установка mongodb. Если нет, скачайте и установите mongodb. Вы можете следовать этой статье для руководства.

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

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

Учитывая наш контекст, мы можем сказать, что users и products — это разные типы, а каждый purchase — это набор пользователей и продуктов. Давайте посмотрим на пример.

Создайте модели каждой категории. Это представления данных, ожидаемых в каждой коллекции.

// User model
const mongoose = require('mongoose')

module.exports = mongoose.model('User', mongoose.Schema({
    name: {
        type: String,
        trim: true,
        required: true
    },
    address: {
        type: String,
        required: true,
        trim: true
    },
    category: {
        type: String,
        enum: ['Platinum', 'Gold', 'Silver', 'Bronze'],
        default: 'Bronze'
    }
}))
// Product model
const mongoose = require('mongoose')

module.exports = mongoose.model('Product', mongoose.Schema({
    name: {
        type: String,
        trim: true,
        required: true
    },
    category: {
        type: String,
        required: true,
        trim: true,
        enum: ['Electronics', 'Toys', 'Kitchen']
    },
    price: {
        type: Number,
        required: true,
        default: 0
    },
    quantity: {
        type: Number,
        required: true,
        min: 0,
        default: 0
    }
}))
// Purchase model
const mongoose = require('mongoose')

module.exports = mongoose.model('Purchase', mongoose.Schema({
    buyer: {
        type: mongoose.Schema.Types.ObjectId,
        required: true,
        ref: 'User'
    },
    product: {
        type: mongoose.Schema.Types.ObjectId,
        required: true,
        ref: 'Product'
    },
    quantity: {
        type: Number,
        required: true,
        min: 1
    }
}))

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

При таком подходе вся наша информация о товарах сохраняется в одной коллекции, а о пользователях — в другой. Мы используем идентификаторы для сопоставления покупок с покупателями и продуктами.

Параметр ref сообщает mongoose, какая коллекция содержит информацию о конкретном поле, и mongoose будет использовать этот путь для заполнения запроса.

Давайте создадим некоторые данные.

const mongoose = require('mongoose')
const data = require('./data.json')
const User = require('./models/user')
const Product = require('./models/product')
const Purchase = require ('./models/purchase')

mongoose.connect('mongodb://localhost:27017/normalize')

mongoose.connection
    .on('open', () => {
      console.log('Mongoose connection open');
    })
    .on('error', (error) => {
      console.log(`Connection error: ${error.message}`);
    });

// Add users to DB.
async function createUsers(users) {
    for(let user of users) {
        try {
            const _user = await User(user).save()
            console.log(`${ _user.name } added to users collection`)
        } catch(error) {
            console.log(`Error: ${ error.message }`)
        }
    }
}

// Add products to DB.
async function createProducts(products) {
    for (let product of products) {
        try {
            const _product = await Product(product).save()
            console.log(`${ _product.name } product added to products collection`)
        } catch(error) {
            console.log(`Error: ${ error.message }`)
        }
    }
}

// Add product order to DB.
async function createPurchases(purchases) {
    for(let purchase of purchases) {
        try {
            const _user = await User.findOne({ name: purchase.user })
            const _product = await Product.findOne({ name: purchase.product })

            await Purchase({
                buyer: _user._id,
                product: _product._id,
                quantity: purchase.quantity
            }).save()

            console.log(`${_product.name} product ordered by ${_user.name}`)

        } catch(error) {
            console.log(`Error: ${ error.message }`)
        }
    }
}

// Get unpopulated purchases data object
async function getPurchases() {
    try {
        const result = await Purchase.find().select('-__v').limit(1)
        console.log(result)
    } catch(error) {
        console.log(`Error: ${ error.message }`)
    }
}

// Get populated purchases data object
async function getPopulatedPurchasesData() {
    try {
        const result = await Purchase.find().populate('buyer').populate('product').limit(1)
        console.log(result)
    } catch(error) {
        console.log(`Error: ${ error.message }`)
    }
}

// Generate all sample data.
async function generateSampleData() {
    await createUsers(data.users)
    await createProducts(data.products)
    await createPurchases(data.purchases)
}

// generateSampleData()
// getPurchases()
// getPopulatedPurchasesData()

Шаг 1.

Прокрутите до конца файла index.js и раскомментируйте функцию generateSampleData. Выполнить node index.js. Обратите внимание на вашу текущую папку, если вы клонировали репозиторий и в данный момент находитесь в корневом каталоге, то вы запустите node normalization/index.js

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

Шаг 2.

Закомментируйте функцию generateSampleData и раскомментируйте функцию getPurchases. Запустите node index.js или node normalization/index.js. Вы должны увидеть данные, напечатанные на вашем терминале. Идентификаторы документов будут отличаться от тех, что на скриншоте, но это не имеет значения.

{
    _id: new ObjectId("63ddfb2af1c0621a61eda7fb"),
    buyer: new ObjectId("63ddfb2af1c0621a61eda7f0"),
    product: new ObjectId("63ddfb2af1c0621a61eda7f7"),
    quantity: 30
}

Ваш ids будет отличаться от приведенных выше, но общая структура должна быть идентичной.

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

Шаг 3.

Закомментируйте функцию getPurchases и раскомментируйте функцию getPopulatedPurchasesData. Запустите node index.js или node normalization/index.js. Вы должны увидеть данные, напечатанные на вашем терминале.

{
    _id: new ObjectId("63ddfb2af1c0621a61eda7fb"),
    buyer: {
      _id: new ObjectId("63ddfb2af1c0621a61eda7f0"),
      name: 'John Cena',
      address: 'California',
      category: 'Bronze'
    },
    product: {
      _id: new ObjectId("63ddfb2af1c0621a61eda7f7"),
      name: 'Buzz Lightyear',
      category: 'Toys',
      price: 10,
      quantity: 50
    },
    quantity: 30
}

Результатом этого запроса является объект с четырьмя полями: _id, покупатель, продукт и количество. Однако поля покупателя и продукта были заполнены информацией из коллекций пользователей и продуктов соответственно.

Преимущества использования нормализации

  • Устраняет избыточность данных. Это означает, что у вас нет дубликатов данных, хранящихся в разных местах вашей базы данных. В нашем примере, если Джон Сина совершает 100 покупок, его личная информация не сохраняется повторно для каждой покупки.
  • Экономит место на диске. Поскольку личная информация хранится один раз в коллекции пользователей, и мы используем идентификатор для создания ссылки, это экономит место, в отличие от дублирования информации для каждой покупки.
  • Обновлять информацию легко. Если пользователь John Cena меняет адрес, нам нужно только отредактировать его информацию в пользовательской коллекции. Это гарантирует согласованность данных.

Недостатки использования нормализации

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

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

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

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

Вы можете найти меня на linkedIn и medium