Предпоставка: Тази статия предполага, че имате някои основни познания за node.js приложения и бази данни (както релационни, така и нерелационни).

Работещите примери използват mongodb и библиотеката mongoose като ODM.

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

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

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

Има три метода за съхраняване на данни в нерелационни бази данни, а именно използване на нормализация, използване на вградени документи и използване на хибриден подход - комбинация от първите два метода.

Начинът, по който избирате да запазвате данни, влияе върху коя страна на уравнението за производителност на заявка за съгласуваност на данни V се опрете.

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

Казано по-просто, ако потребител A и B изпълнят една и съща заявка в една и съща база данни, и двамата трябва да получат едни и същи данни и ако потребител B актуализира тези данни, потребител A трябва при поискване да получи актуализираното копие на тези данни.

За да се осигури съгласуваност на данните, бяха създадени концепции като транзакции в бази данни, нормализация и т.н., за да управляват и организират свързани данни в бази данни.

В тази статия ще обсъдим концепцията за нормализация.

Нормализирането просто означава, че не дублирайте данни във вашата база данни.

Ако едни и същи данни се изискват многократно в базата данни, съхранете тези данни в различна колекция, дайте им уникален id и използвайте този id като препратка в други области на вашата база данни, където тази информация се изисква.

Това елиминира дублирането на данни и проблемите с поддръжката, тъй като актуализациите на данните се правят само на едно място.

В релационни бази данни, в една SQL заявка, можете да join данни от различни таблици, следователно нормализирането (разделяне на данни) несъмнено е добро за релационни бази данни, но с нерелационни бази данни не е толкова просто.

Нормализирането може да не е толкова добро за нерелационни бази данни.

Това може да не е най-популярното нещо за казване през 2022 г., но не убивайте месинджъра 😆.

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

Можете да получите достъп до всички примери на код от това хранилище. Файловете се записват в директории (вградени, нормализирани и хибридни). Всеки има отделен файл data.json и index.js.

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

Този пример използва mongoose версия 6.1.8 като ODM библиотека

Проверете дали имате локална инсталация на 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