В этом руководстве я продемонстрирую, как использовать SQLite в сочетании с JavaScript внутри среды Node.js с помощью драйвера sqlite3 Node.js. Для тех, кто не знаком с SQLite , это простая однофайловая реляционная база данных, которая очень популярна среди интеллектуальных устройств, встроенных систем и даже небольших веб-приложений.
Установка и установка
Я начну с создания нового пакета npm с помощью npm init
внутри пустого
каталога под названием node-sqlite-tutorial.
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sane defaults.
See `npm help json` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg> --save` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
name: (app) node-sqlite
version: (0.0.0) 0.1.0
description: Code for tutorial blog on node and sqlite
entry point: (index.js) main.js
test command:
git repository:
keywords:
author: Adam McQuistan
license: (BSD) MIT
About to write to /node-sqlite/app/package.json:
{
"name": "node-sqlite",
"version": "0.1.0",
"description": "Code for tutorial blog on node and sqlite",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": "",
"author": "Adam McQuistan",
"license": "MIT"
}
Is this ok? (yes)
Затем мне нужно будет установить пакет sqlite3 через npm следующим образом:
$ npm install --save sqlite3
В дополнение к sqlite3 я собираюсь установить Bluebird, чтобы использовать знакомую функциональность обещаний при программировании базы данных.
$ npm install --save bluebird
Теперь я создам пустой файл рядом с файлом package.json с именем database.sqlite3, в котором SQLite будет хранить данные.
Проектирование базы данных
Как и почти во всех других моих статьях, я буду использовать придуманное приложение, чтобы помочь описать некоторые важные аспекты программирования баз данных с помощью Node.js и SQLite. В этой статье я исхожу из предположения, что я создаю уровень доступа к данным для проекта и приложения для отслеживания задач. Основные бизнес-правила для уровня доступа к данным этого приложения следующие:
- В приложении есть проекты
- У каждого проекта может быть одна или несколько задач, которые нужно выполнить.
С изложенными бизнес-правилами я могу взять эту информацию и приступить к разработке необходимых таблиц и их полей. Понятно, что мне понадобится таблица проектов и таблица задач . В остальном я просто воспользуюсь небольшой интуицией, некоторыми составленными тестовыми данными и буду работать с ними (обычная рабочая характеристика для большинства разработчиков).
таблица проектов
я бы название
1 Написать Node.js - Учебное пособие по SQLite
таблица задач
я бы название описание завершено projectId
1 Контур Обзор разделов на высоком уровне 1 1 2 Писать Напишите содержание статьи и примеры кода 0 1
Хорошо, теперь, когда я знаю, что мне нужно создать, я могу перевести это в код.
Создание базы данных
Для начала мне нужно создать файл main.js вместе с файлом dao.js (или объектом доступа к данным ) в том же каталоге, что и файл package.json.
Внутри dao.js я добавлю импорт для объектов Promise
AppDAO
класс
доступа к данным под названием AppDAO, который установит соединение с
базой данных внутри конструктора и назначит его полю-члену под названием
db
.
// dao.js
const sqlite3 = require('sqlite3')
const Promise = require('bluebird')
class AppDAO {
constructor(dbFilePath) {
this.db = new sqlite3.Database(dbFilePath, (err) => {
if (err) {
console.log('Could not connect to database', err)
} else {
console.log('Connected to database')
}
})
}
}
module.exports = AppDAO
Связь довольно проста. Вы просто создаете экземпляр Database
sqlite3,
передавая ему путь к файлу базы данных SQLite, к которому вы хотите
подключиться, и, при необходимости, проверяйте наличие ошибок, которые
могут возникнуть. Как отмечалось выше, я сохраняю этот объект
подключения в поле с именем db
в классе AppDAO
Я продолжу объяснять, как использовать объект подключения для отправки запросов в базу данных. Пакет sqlite3 Node.js предоставляет несколько различных методов для выполнения запросов, но в этом руководстве я остановлюсь на следующих:
run
: используется для создания или изменения таблиц, а также для вставки или обновления данных таблицыget
: выбрать одну строку данных из одной или нескольких таблицall
: выбрать несколько строк данных из одной или нескольких таблиц
Для начала я хотел бы изучить метод run
Его общий синтаксис выглядит
так:
db.run('SOME SQL QUERY', [param1, param2], (err) => {
if (err) {
console.log('ERROR!', err)
}
})
Первый параметр, переданный в run(...)
представляет собой строку SQL,
которая будет выполняться, и является единственным обязательным
параметром. Второй - это необязательный массив параметров, которые
библиотека sqlite3 заменит для любого символа '?' заполнители в
запросе (я немного продемонстрирую это). Финал - это функция обратного
вызова ошибки.
Как вы могли догадаться, я буду использовать run(...)
для создания и
обновления моих проектов и задач. Однако на самом деле я собираюсь
обернуть его в свою собственную версию метода run
AppDAO
потому что
я хотел бы инкапсулировать его в bluebird
Promise
чтобы сделать вещи
явно асинхронными и основанными на обещаниях, например:
// dao.js
const sqlite3 = require('sqlite3')
const Promise = require('bluebird')
class AppDAO {
// omitting constructor code
run(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.run(sql, params, function (err) {
if (err) {
console.log('Error running sql ' + sql)
console.log(err)
reject(err)
} else {
resolve({ id: this.lastID })
}
})
})
}
}
С помощью моего собственного AppDAO.run(...)
теперь я могу
использовать его для создания таблиц продуктов и задач.
Для начала я добавляю в свой проект еще два файла: project_repository.js
и task_repository.js. Внутри project_repository.js я определяю класс под
названием ProjectRepository
, который имеет конструктор , который
принимает экземпляр AppDAO
объекта и createTable
метод , который
выполняет некоторые DDL (Definition Language Data) SQL следующим
образом:
// project_repository.js
class ProjectRepository {
constructor(dao) {
this.dao = dao
}
createTable() {
const sql = `
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT)`
return this.dao.run(sql)
}
}
module.exports = ProjectRepository;
Затем я снова делаю то же самое, но на этот раз в файле task_repository.js.
// task_repository.js
class TaskRepository {
constructor(dao) {
this.dao = dao
}
createTable() {
const sql = `
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
description TEXT,
isComplete INTEGER DEFAULT 0,
projectId INTEGER,
CONSTRAINT tasks_fk_projectId FOREIGN KEY (projectId)
REFERENCES projects(id) ON UPDATE CASCADE ON DELETE CASCADE)`
return this.dao.run(sql)
}
}
module.exports = TaskRepository;
DDL SQL для создания таблиц готов, поэтому я перейду к методам вставки данных в таблицы.
Вставка данных
В ProjectRepository
мне нужно добавить create
который получает имя
проекта для создания и выполняет соответствующий оператор INSERT с
помощью AppDAO.run(...)
. Обратите внимание, как я использовал '?'
для представления значения имени проекта, а затем поместите name
в
необязательный аргумент массива params в метод run(...)
. Это
называется параметризованным оператором запроса, который очищает входные
данные, чтобы минимизировать риски внедрения SQL.
// project_repository.js
class ProjectRepository {
// omitting other methods
create(name) {
return this.dao.run(
'INSERT INTO projects (name) VALUES (?)',
[name])
}
}
module.exports = ProjectRepository;
Аналогичный метод создания необходим для класса TaskRepository
// task_repository.js
class TaskRepository {
// omitting other methods
create(name, description, isComplete, projectId) {
return this.dao.run(
`INSERT INTO tasks (name, description, isComplete, projectId)
VALUES (?, ?, ?, ?)`,
[name, description, isComplete, projectId])
}
}
module.exports = TaskRepository;
Теперь, когда у меня есть возможность ВСТАВЛЯТЬ данные в базу данных, я хотел бы добавить функции для ее обновления.
Обновление данных
В ProjectRepository
я добавлю update
который принимает project
и
снова обновляет все поля для записи базы данных этого проекта, используя
метод AppDAO.run(...)
, например:
// project_repository.js
class ProjectRepository {
// omitting other methods
update(project) {
const { id, name } = project
return this.dao.run(
`UPDATE projects SET name = ? WHERE id = ?`,
[name, id]
)
}
}
module.exports = ProjectRepository;
Далее нужно добавить соответствующий метод обновления в класс
TaskRepository
// task_repository.js
class TaskRepository {
// omitting other methods
update(task) {
const { id, name, description, isComplete, projectId } = task
return this.dao.run(
`UPDATE tasks
SET name = ?,
description = ?,
isComplete = ?,
projectId = ?
WHERE id = ?`,
[name, description, isComplete, projectId, id]
)
}
}
module.exports = TaskRepository;
Удаление данных
Последняя мутационная функция, которую необходимо реализовать, - это
предоставить возможность удалять записи из базы данных. Для этого я
снова буду использовать метод AppDAO.run(...)
в сочетании с новыми
методами delete
для классов ProjectRepository
и TaskRepository
Для ProjectRepository
это выглядит так:
// project_repository.js
class ProjectRepository {
// omitting other methods
delete(id) {
return this.dao.run(
`DELETE FROM projects WHERE id = ?`,
[id]
)
}
}
module.exports = ProjectRepository;
А для TaskRepository
это выглядит так:
// task_repository.js
class TaskRepository {
// omitting other methods
delete(id) {
return this.dao.run(
`DELETE FROM tasks WHERE id = ?`,
[id]
)
}
}
module.exports = TaskRepository;
Хорошо, это завершает все способы, которыми я буду использовать метод
run
Далее я представлю два других связанных get
и all
методы
пакета sqlite3 Node.js.
Чтение данных
В этом разделе я собираюсь рассказать, как использовать get
и all
методы библиотеки sqlite3 Node.js. Как упоминалось ранее, get
используется для получения одной строки данных, в то время как all
используется для запроса многих строк данных.
Базовый синтаксис использования get
выглядит так:
db.get('SELECT ...', [param1, param2], (err, result) => {
if (err) {
console.log(err)
} else {
// do something with result
}
})
Где db
- объект подключения sqlite3. Вы заметите, что синтаксис по
существу идентичен run
за исключением того, что обратный вызов имеет
дополнительный параметр, который содержит объект результата запроса, при
условии, что ошибки не было.
Базовый синтаксис для all
по сути снова тот же, за исключением того,
что второй параметр обратного вызова представляет собой массив
результатов, возвращаемых запросом, например:
db.all('SELECT ...', [param1, param2], (err, results) => {
if (err) {
console.log(err)
} else {
// do something with results
}
})
Так же , как я сделал с sqlite3 run
метода я собираюсь реализовать
get
и all
методов , использующих bluebird
Promise
в AppDAO
класса , как показано ниже:
// dao.js
const sqlite3 = require('sqlite3').verbose()
const Promise = require('bluebird')
class AppDAO {
// omitting other methods
get(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.get(sql, params, (err, result) => {
if (err) {
console.log('Error running sql: ' + sql)
console.log(err)
reject(err)
} else {
resolve(result)
}
})
})
}
all(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.all(sql, params, (err, rows) => {
if (err) {
console.log('Error running sql: ' + sql)
console.log(err)
reject(err)
} else {
resolve(rows)
}
})
})
}
}
Теперь я могу использовать эти методы в ProjectRepository
и
TaskRepository
для извлечения данных из базы данных SQLite.
Для начала я добавлю getById
чтобы выбирать их записи по id.
В ProjectRepository
я добавляю это:
// project_repository.js
class ProjectRepository {
// omitting other methods
getById(id) {
return this.dao.get(
`SELECT * FROM projects WHERE id = ?`,
[id])
}
}
module.exports = ProjectRepository;
И в TaskRepository
аналогично:
// task_repository.js
class TaskRepository {
// omitting other methods
getById(id) {
return this.dao.get(
`SELECT * FROM tasks WHERE id = ?`,
[id])
}
}
module.exports = TaskRepository;
Чтобы продемонстрировать метод AppDAO.all(...)
я добавлю возможность
выбирать все проекты, а также все задачи для данного проекта.
Код для выбора всех проектов выглядит так:
// project_repository.js
class ProjectRepository {
// omitting other methods
getAll() {
return this.dao.all(`SELECT * FROM projects`)
}
}
module.exports = ProjectRepository;
Затем, чтобы выбрать все задачи для проекта, я буду использовать метод
getTasks(projectId)
который ожидает идентификатор проекта, для
которого вы хотите получить задачи.
// project_repository.js
class ProjectRepository {
// omitting other methods
getTasks(projectId) {
return this.dao.all(
`SELECT * FROM tasks WHERE projectId = ?`,
[projectId])
}
}
module.exports = ProjectRepository;
Использование кода доступа к данным
До сих пор я в основном создал библиотеку доступа к данным для этого вымышленного проекта и приложения для отслеживания задач. Сейчас я хотел бы использовать его для загрузки моих тестовых данных, отображаемых в таблицах в разделе « Проектирование базы данных ».
В файле main.js я хочу TaskRepository
классы AppDAO
,
ProjectRepository
и require
. Затем я буду использовать их для
создания таблиц, заполнения их данными, извлечения данных из базы данных
и отображения на консоли.
// main.js
const Promise = require('bluebird')
const AppDAO = require('./dao')
const ProjectRepository = require('./project_repository')
const TaskRepository = require('./task_repository')
function main() {
const dao = new AppDAO('./database.sqlite3')
const blogProjectData = { name: 'Write Node.js - SQLite Tutorial' }
const projectRepo = new ProjectRepository(dao)
const taskRepo = new TaskRepository(dao)
let projectId
projectRepo.createTable()
.then(() => taskRepo.createTable())
.then(() => projectRepo.create(blogProjectData.name))
.then((data) => {
projectId = data.id
const tasks = [
{
name: 'Outline',
description: 'High level overview of sections',
isComplete: 1,
projectId
},
{
name: 'Write',
description: 'Write article contents and code examples',
isComplete: 0,
projectId
}
]
return Promise.all(tasks.map((task) => {
const { name, description, isComplete, projectId } = task
return taskRepo.create(name, description, isComplete, projectId)
}))
})
.then(() => projectRepo.getById(projectId))
.then((project) => {
console.log(`\nRetreived project from database`)
console.log(`project id = ${project.id}`)
console.log(`project name = ${project.name}`)
return projectRepo.getTasks(project.id)
})
.then((tasks) => {
console.log('\nRetrieved project tasks from database')
return new Promise((resolve, reject) => {
tasks.forEach((task) => {
console.log(`task id = ${task.id}`)
console.log(`task name = ${task.name}`)
console.log(`task description = ${task.description}`)
console.log(`task isComplete = ${task.isComplete}`)
console.log(`task projectId = ${task.projectId}`)
})
})
resolve('success')
})
.catch((err) => {
console.log('Error: ')
console.log(JSON.stringify(err))
})
}
main()
Запустите с помощью node
следующим образом:
$ node main.js
И вы увидите результат, как показано ниже.
Connected to database
Retreived project from database
project id = 1
project name = 1
Retrieved project tasks from database
task id = 1
task name = Outline
task description = High level overview of sections
task isComplete = 1
task projectId = 1
task id = 2
task name = Write
task description = Write article contents and code examples
task isComplete = 0
task projectId = 1
Заключение
В этом руководстве я рассмотрел основы API пакета Node.js sqlite3 и продемонстрировал, как вы можете обернуть эту функциональность в объектно-ориентированный JavaScript с акцентом на асинхронную реализацию на основе Promise.
Как всегда, я благодарю вас за чтение и приветствую комментарии и критику ниже.