Предпоставка: Тази статия предполага, че имате някои основни познания за 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 се стартира нова заявка. Процесът на заявка използва определения тип за търсене и сравняване на съответните типове в колекцията, докато се намери съвпадение.
Това е най-големият недостатък, колкото повече полета трябва да попълним, толкова по-дълго е изпълнението на заявката, защото тя трябва да премине през процеса на търсене, съвпадение и връщане.
Ето защо, преди да изберете да използвате нормализиране в нерелационни бази данни, е важно да обмислите колко полета трябва да попълните, необходима ли е тази информация в критичен момент от пътуването на вашия потребител, където скоростта е от първостепенно значение?, има ли алтернативен метод за запазване на информация, която най-добре ще отговори на изискваните нужди? и т.н.
След това ще обсъдим как да използваме вградени документи, ще разгледаме предимствата, които има пред нормализирането и кой контекст е най-подходящ, но дотогава ви благодаря за четенето, надяваме се, че ще отделите време за следващата статия.