Магазин на JavaScript, часть 1 из 19. Серверное приложение, база данных, ORM Sequelize
10.11.2021
Теги: Backend • Frontend • JavaScript • ORM • React.js • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Корзина • Фреймворк
Простой интернет-магазин на Node.js (сервер) и React.js (клиент). Данные будем хранить в базе данных PostgreSQL. Для серверной части используем фреймворк Express.js. Приложение не имеет практической ценности, сделано с целью изучения.
Простой сервер
Создаем директорию проекта shop
, внутри нее — еще две директории, server
и client
. Переходим в директорию shop/server
, создаем проект:
> npm init -y
Устанавливаем зависимости, которые нам потребуются в работе:
> npm install express pg pg-hstore sequelize cors dotenv
Чтобы отслеживать изменения в коде и перезапускать сервер, установим как dev-зависимость пакет nodemon
:
> npm install nodemon --save-dev
Внесем изменения в файл package.json
— добавим в него команду запуска сервера через nodemon
и type:module
(чтобы использовать import
вместо require
).
{ "name": "shop-server", "version": "1.0.0", "description": "Интернет магазин, сервер", "main": "index.js", "type": "module", "scripts": { "start": "node index.js", "start-dev": "nodemon index.js" }, "keywords": [ "Backend", "JavaScript", "Node.js", "Express.js" ], "author": "Евгений Токмаков", "license": "ISC", "devDependencies": { "nodemon": "^2.0.15" }, "dependencies": { "cors": "^2.8.5", "dotenv": "^10.0.0", "express": "^4.17.1", "pg": "^8.7.1", "pg-hstore": "^2.3.4", "sequelize": "^6.9.0" } }
Создаем файл index.js
, добавляем в него следующий код:
import express from 'express' const PORT = 7000 const app = express() app.listen(PORT, () => console.log('Сервер запущен на порту', PORT))
Запускаем сервер в режиме разработки через nodemon
:
> npm run start-dev [nodemon] 2.0.15 [nodemon] to restart at any time, enter `rs` [nodemon] watching path(s): *.* [nodemon] watching extensions: js,mjs,json [nodemon] starting `node index.js` Сервер запущен на порту 7000
Вся конфигурация у нас будет в переменных окружения, создаем файл .env
:
PORT=7000
И будем получать номер порта в index.js
следующим образом:
import config from 'dotenv/config' import express from 'express' const PORT = process.env.PORT || 5000 const app = express() app.listen(PORT, () => console.log('Сервер запущен на порту', PORT))
База данных
Идем на сайт postgresql.org
, в раздел Downloads
— и скачиваем последнюю версию. Установка там простая, так что не будем на этом останавливаться. Вместе с базой данных будет установлен клиент pgAdmin
для работы с сервером БД — запускаем его и создаем БД online_store
.
Будем использовать ORM-библиотеку Sequelize, которая осуществляет сопоставление таблиц в БД и отношений между ними с классами моделей. Модели мы создадим чуть позже, а пока создаем файл sequelize.js
, где укажем настройки подключения к серверу базы данных, сами настройки получим из файла .env
.
import {Sequelize} from 'sequelize' export default new Sequelize( process.env.DB_NAME, // база данных process.env.DB_USER, // пользователь process.env.DB_PASS, // пароль { dialect: 'postgres', host: process.env.DB_HOST, port: process.env.DB_PORT } )
PORT=7000 DB_HOST=localhost DB_NAME=online_store DB_USER=postgres DB_PASS=qwerty DB_PORT=5432
Вносим изменения в index.js
, чтобы перед запуском сервера установить соединение с базой данных:
import config from 'dotenv/config' import express from 'express' import sequelize from './sequelize.js' const PORT = process.env.PORT || 5000 const app = express() const start = async () => { try { await sequelize.authenticate() await sequelize.sync() app.listen(PORT, () => console.log('Сервер запущен на порту', PORT)) } catch(e) { console.log(e) } } start()
Метод sync()
синхронизирует структуру базы данных с определением моделей. Например, если для какой-то модели отсутствует соответствующая таблица в БД, то эта таблица создается.
ORM (Object-relational mapping)
Создаем директориюmodels
, внутри нее — файл mapping.js
, добавляем в него следующий код:
import sequelize from '../sequelize.js' import database from 'sequelize' const { DataTypes } = database /* * Описание моделей */ // модель «Пользователь», таблица БД «users» const User = sequelize.define('user', { id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, email: {type: DataTypes.STRING, unique: true}, password: {type: DataTypes.STRING}, role: {type: DataTypes.STRING, defaultValue: 'USER'}, }) // модель «Корзина», таблица БД «baskets» const Basket = sequelize.define('basket', { id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, }) // связь между корзиной и товаром через промежуточную таблицу «basket_products» // у этой таблицы будет составной первичный ключ (basket_id + product_id) const BasketProduct = sequelize.define('basket_product', { quantity: {type: DataTypes.INTEGER, defaultValue: 1}, }) // модель «Товар», таблица БД «products» const Product = sequelize.define('product', { id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, name: {type: DataTypes.STRING, unique: true, allowNull: false}, price: {type: DataTypes.INTEGER, allowNull: false}, rating: {type: DataTypes.INTEGER, defaultValue: 0}, image: {type: DataTypes.STRING, allowNull: false}, }) // модель «Категория», таблица БД «categories» const Category = sequelize.define('category', { id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, name: {type: DataTypes.STRING, unique: true, allowNull: false}, }) // модель «Бренд», таблица БД «brands» const Brand = sequelize.define('brand', { id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, name: {type: DataTypes.STRING, unique: true, allowNull: false}, }) // связь между товаром и пользователем через промежуточную таблицу «rating» // у этой таблицы будет составной первичный ключ (product_id + user_id) const Rating = sequelize.define('rating', { rate: {type: DataTypes.INTEGER, allowNull: false}, }) // свойства товара, у одного товара может быть много свойств const ProductProp = sequelize.define('product_prop', { id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, name: {type: DataTypes.STRING, allowNull: false}, value: {type: DataTypes.STRING, allowNull: false}, }) /* * Описание связей */ // связь many-to-many товаров и корзин через промежуточную таблицу basket_products; // товар может быть в нескольких корзинах, в корзине может быть несколько товаров Basket.belongsToMany(Product, { through: BasketProduct, onDelete: 'CASCADE' }) Product.belongsToMany(Basket, { through: BasketProduct, onDelete: 'CASCADE' }) // super many-to-many https://sequelize.org/master/manual/advanced-many-to-many.html // это обеспечит возможность любых include при запросах findAll, findOne, findByPk Basket.hasMany(BasketProduct) BasketProduct.belongsTo(Basket) Product.hasMany(BasketProduct) BasketProduct.belongsTo(Product) // связь категории с товарами: в категории может быть несколько товаров, но // каждый товар может принадлежать только одной категории Category.hasMany(Product, {onDelete: 'RESTRICT'}) Product.belongsTo(Category) // связь бренда с товарами: у бренда может быть много товаров, но каждый товар // может принадлежать только одному бренду Brand.hasMany(Product, {onDelete: 'RESTRICT'}) Product.belongsTo(Brand) // связь many-to-many товаров и пользователей через промежуточную таблицу rating; // за один товар могут проголосовать несколько зарегистрированных пользователей, // один пользователь может проголосовать за несколько товаров Product.belongsToMany(User, {through: Rating, onDelete: 'CASCADE'}) User.belongsToMany(Product, {through: Rating, onDelete: 'CASCADE'}) // super many-to-many https://sequelize.org/master/manual/advanced-many-to-many.html // это обеспечит возможность любых include при запросах findAll, findOne, findByPk Product.hasMany(Rating) Rating.belongsTo(Product) User.hasMany(Rating) Rating.belongsTo(User) // связь товара с его свойствами: у товара может быть несколько свойств, но // каждое свойство связано только с одним товаром Product.hasMany(ProductProp, {as: 'props', onDelete: 'CASCADE'}) ProductProp.belongsTo(Product) export { User, Basket, Product, Category, Brand, Rating, BasketProduct, ProductProp, Order, OrderItem }
В index.js
импортируем модели, чтобы при вызове метода sync()
были созданы все таблицы:
/* .......... */ import * as mapping from './models/mapping.js'; /* .......... */
-- Дамп структуры для таблицы public.baskets CREATE TABLE IF NOT EXISTS "baskets" ( "id" INTEGER NOT NULL DEFAULT 'nextval(...)', "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, PRIMARY KEY ("id") ) -- Дамп структуры для таблицы public.basket_products CREATE TABLE IF NOT EXISTS "basket_products" ( "quantity" INTEGER NULL DEFAULT '1', "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, "basket_id" INTEGER NOT NULL, "product_id" INTEGER NOT NULL, PRIMARY KEY ("basket_id", "product_id"), CONSTRAINT "basket_products_basket_id_fkey" FOREIGN KEY ("basket_id") REFERENCES "public"."baskets" ("id") ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT "basket_products_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "public"."products" ("id") ON UPDATE CASCADE ON DELETE CASCADE ); -- Дамп структуры для таблицы public.brands CREATE TABLE IF NOT EXISTS "brands" ( "id" INTEGER NOT NULL DEFAULT 'nextval(...)', "name" VARCHAR(255) NOT NULL, "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, PRIMARY KEY ("id"), UNIQUE INDEX "brands_name_key" ("name") ); -- Дамп структуры для таблицы public.categories CREATE TABLE IF NOT EXISTS "categories" ( "id" INTEGER NOT NULL DEFAULT 'nextval(...)', "name" VARCHAR(255) NOT NULL, "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, PRIMARY KEY ("id"), UNIQUE INDEX "categories_name_key" ("name") ); -- Дамп структуры для таблицы public.products CREATE TABLE IF NOT EXISTS "products" ( "id" INTEGER NOT NULL DEFAULT 'nextval(...)', "name" VARCHAR(255) NOT NULL, "price" INTEGER NOT NULL, "rating" INTEGER NULL DEFAULT '0', "image" VARCHAR(255) NOT NULL, "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, "category_id" INTEGER NULL DEFAULT NULL, "brand_id" INTEGER NULL DEFAULT NULL, PRIMARY KEY ("id"), UNIQUE INDEX "products_name_key" ("name"), CONSTRAINT "products_brand_id_fkey" FOREIGN KEY ("brand_id") REFERENCES "public"."brands" ("id") ON UPDATE CASCADE ON DELETE RESTRICT, CONSTRAINT "products_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "public"."categories" ("id") ON UPDATE CASCADE ON DELETE RESTRICT ); -- Дамп структуры для таблицы public.product_props CREATE TABLE IF NOT EXISTS "product_props" ( "id" INTEGER NOT NULL DEFAULT 'nextval(...)', "name" VARCHAR(255) NOT NULL, "value" VARCHAR(255) NOT NULL, "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, "product_id" INTEGER NULL DEFAULT NULL, PRIMARY KEY ("id"), CONSTRAINT "product_props_product_id_fkey" FOREIGN KEY ("product_id" REFERENCES "public"."products" ("id") ON UPDATE CASCADE ON DELETE CASCADE ); -- Дамп структуры для таблицы public.ratings CREATE TABLE IF NOT EXISTS "ratings" ( "rate" INTEGER NOT NULL, "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, "product_id" INTEGER NOT NULL, "user_id" INTEGER NOT NULL, PRIMARY KEY ("product_id", "user_id"), CONSTRAINT "ratings_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "public"."products" ("id") ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT "ratings_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON UPDATE CASCADE ON DELETE CASCADE ); -- Дамп структуры для таблицы public.users CREATE TABLE IF NOT EXISTS "users" ( "id" INTEGER NOT NULL DEFAULT 'nextval(...)', "email" VARCHAR(255) NULL DEFAULT NULL, "password" VARCHAR(255) NULL DEFAULT NULL, "role" VARCHAR(255) NULL DEFAULT 'USER', "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, PRIMARY KEY ("id"), UNIQUE INDEX "users_email_key" ("email") );
Кроме описанных нами полей у каждой таблицы будут созданы поля createdAt
и updatedAt
типа datetime
— это время создания и последнего обновления строки в таблице. Если эти поля не нужны — редактируем файл sequelize.js
:
import {Sequelize} from 'sequelize' export default new Sequelize( process.env.DB_NAME, // база данных process.env.DB_USER, // пользователь process.env.DB_PASS, // пароль { dialect: 'postgres', host: process.env.DB_HOST, port: process.env.DB_PORT, define: { underscored: true, // использовать snake_case вместо camelCase для полей таблиц БД timestamps: false, // не добавлять поля created_at и updated_at при создании таблиц } } )
- Магазин на JavaScript, часть 19 из 19. Редактирование характеристик и рефакторинг приложения
- Магазин на JavaScript, часть 18 из 19. Панель управления: редактирование категорий и брендов
- Магазин на JavaScript, часть 17 из 19. Панель управления: список заказов, категорий и брендов
- Магазин на JavaScript, часть 15 из 19. Работа с заказами на сервере, оформление заказа
- Магазин на JavaScript, часть 14 из 19. Кнопка «Назад», страница товара, корзина покупателя
- Магазин на JavaScript, часть 13 из 19. Хранилище каталога, компонент витрины, кнопка «Назад»
- Магазин на JavaScript, часть12 из 19. Запросы на сервер, состояние приложения, Signup и Login
Поиск: JavaScript • ORM • React.js • Web-разработка • Frontend • Backend • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк