Кто пишет на Node.js и использует MySQL, тот непременно знает, что наш дорогой товарищ Felix Geisendörfer три года назад совершил героический и самоотверженный подвиг: в одиночку разработал очень качественный драйвер для подключения к MySQL, нативно реализовав двоичный протокол этой СУБД на JavaScript. Потом к проекту подключились другие уважаемые товарищи, была реализована поддержка пулов соединений, кластеров, транзакций, восстановление при временной утере соединения и т.д. Сейчас драйвер является самым развитым, хорошо проработанным и активно поддерживаемым из того, что мы имеем в открытых репозитариях npm и github. Удивительно даже, что при хорошо проработанной низкоуровневой реализации, все удобства, которые предоставляет это драйвер для прикладного разработчика, сводятся к одному методу query. Для меня лично, этого очень мало, ну привык дедушка к удобствам для возврата скалярных значений, строк и столбцов в массивы, интроспекции структур БД. Так что, с удовольствием делюсь этими наработками с вами, мои дорогие читатели, но предупреждаю, что все удобства будут на улице. Есть конечно вариант тесной интеграции с драйвером, но FelixGe желает оставить драйвер исключительно низкоуровневым, поэтому я остановился на варианте внешней библиотеки с добавлением к драйверу через примеси. В виде примесей удобства попали и в платформу Impress, а так же, опубликованы как патч к драйверу. О функционале и вариантах использования далее.
Удобства выборки данных
Далее, под словом «возвращает» буду разуметь второй параметр callback, а не результат вызова функции.
Выборка одной записи: connection.queryRow(sql, values, callback) возвращает хеш (ассоциативный массив), в котором имена полей становятся ключами (вместо того, чтобы получать массив в массиве через query).
connection.queryRow('SELECT * FROM Language where LanguageId=?', [3], function(err, row) {
console.dir({queryRow:row});
/* Example:
queryRow: {
LanguageId: 3,
LanguageName: 'Russian',
LanguageSign: 'ru',
LanguageISO: 'ru',
Caption: 'Русский'
}
*/
});
Выборка скаляра (то есть единичного значения): connection.queryValue(sql, values, callback) возвращает одно значение (вместо того, чтобы получать массив в массиве с одним значением). Удобно при выборке одного пол из одной записи, например Id по имени с указанием LIMIT 1 или функций count(*), max(field) и т.д.
connection.queryValue('SELECT LanguageName FROM Language where LanguageId=?', [8],
function(err, name) {
console.dir({queryValue:name});
/* Example:
queryValue: 'Italiano'
*/
});
Выборка одной колонки: connection.queryArray(sql, values, callback) возвращает массив, заполненный значениями одного поля для каждой записи, из результата выполнения запроса. То есть, это выборка вертикальной колонки, в отличие от выборки горизонтальной записи queryRow.
connection.queryArray('SELECT LanguageSign FROM Language', [], function(err, result) {
console.dir({queryHash:result});
/* Example:
queryArray: [ 'de', 'en', 'es', 'fr', 'it', 'pl', 'ru', 'ua' ]
*/
});
Выборка хеша : connection.queryHash(sql, values, callback) возвращает хеш (ассоциативный массив) двухуровневой вложенности, где ключи первого уровня — значения первого поля из результата выполнения запроса, а ключи второго уровня — все поля результата запроса (включая и первое).
connection.queryHash(
'SELECT LanguageSign, LanguageId, LanguageName, Caption, LanguageISO FROM Language', [],
function(err, result) {
console.dir({queryHash:result});
/* Example:
queryHash: {
en: {
LanguageSign: 'en',
LanguageId: 2,
LanguageName: 'English',
Caption: 'Английский',
LanguageISO: 'en' },
ru: {
LanguageSign: 'ru',
LanguageId: 3,
LanguageName: 'Russian',
Caption: 'Русский',
LanguageISO: 'ru' },
de: {
LanguageSign: 'de',
LanguageId: 7,
LanguageName: 'Deutsch',
Caption: 'Немецкий',
LanguageISO: 'de' },
it: {
LanguageSign: 'it',
LanguageId: 8,
LanguageName: 'Italiano',
Caption: 'Итальянский',
LanguageISO: 'it'
}
}
*/
});
Выборка пар ключ/значение: connection.queryKeyValue(sql, values, callback) возвращает хеш (ассоциативный массив), где ключом будет первое поле из результата запроса запросе.
connection.queryKeyValue(
'SELECT LanguageISO, LanguageName FROM Language', [], function(err, keyValue) {
console.dir({queryKeyValue:keyValue});
/* Example:
keyValue: {
en: 'English',
ru: 'Russian',
uk: 'Ukrainian',
es: 'Espanol',
fr: 'Francais',
de: 'Deutsch',
it: 'Italiano',
pl: 'Poliski'
}
*/
});
Удобства интроспекции
То есть, удобства получения метаданных, структур и параметров базы для их анализа и автоматического построения логики или интерфейсов по работы с этой базой.
Получение первичного ключа: connection.primary(table, callback) возвращает хеш (ассоциативный массив), с метаданными о первичном ключе, см. набор метаданных в примере.
connection.primary('Language', function(err, primary) {
console.dir({primary:primary});
/* Example:
primary: {
Table: 'language',
Non_unique: 0,
Key_name: 'PRIMARY',
Seq_in_index: 1,
Column_name: 'LanguageId',
Collation: 'A',
Cardinality: 9,
Sub_part: null,
Packed: null,
Null: '',
Index_type: 'BTREE',
Comment: '',
Index_comment: ''
}
*/
});
Получение внешних ключей: connection.foreign(table, callback) возвращает хеш (ассоциативный массив), с двойной вложенностью, на первом уровне имена внешних ключей, а на втором — метаданные, описывающие этот ключ. Набор полей см. в примере.
connection.foreign('TemplateCaption', function(err, foreign) {
console.dir({foreign:foreign});
/* Example:
foreign: {
fkTemplateCaptionLanguage: {
CONSTRAINT_NAME: 'fkTemplateCaptionLanguage',
COLUMN_NAME: 'LanguageId',
ORDINAL_POSITION: 1,
POSITION_IN_UNIQUE_CONSTRAINT: 1,
REFERENCED_TABLE_NAME: 'language',
REFERENCED_COLUMN_NAME: 'LanguageId' },
fkTemplateCaptionTemplate: {
CONSTRAINT_NAME: 'fkTemplateCaptionTemplate',
COLUMN_NAME: 'TemplateId',
ORDINAL_POSITION: 1,
POSITION_IN_UNIQUE_CONSTRAINT: 1,
REFERENCED_TABLE_NAME: 'template',
REFERENCED_COLUMN_NAME: 'TemplateId'
}
*/
});
Получение ограничений целостности: connection.constraints(table, callback) возвращает хеш (ассоциативный массив), с двойной вложенностью, на первом уровне имена ограничений целостности, а на втором — метаданные, описывающие каждое ограничение. Набор полей см. в примере.
connection.constraints('TemplateCaption', function(err, constraints) {
console.dir({constraints:constraints});
/* Example:
constraints: {
fkTemplateCaptionLanguage: {
CONSTRAINT_NAME: 'fkTemplateCaptionLanguage',
UNIQUE_CONSTRAINT_NAME: 'PRIMARY',
REFERENCED_TABLE_NAME: 'Language',
MATCH_OPTION: 'NONE',
UPDATE_RULE: 'RESTRICT',
DELETE_RULE: 'CASCADE' },
fkTemplateCaptionTemplate: {
CONSTRAINT_NAME: 'fkTemplateCaptionTemplate',
UNIQUE_CONSTRAINT_NAME: 'PRIMARY',
REFERENCED_TABLE_NAME: 'Template',
MATCH_OPTION: 'NONE',
UPDATE_RULE: 'RESTRICT',
DELETE_RULE: 'CASCADE'
}
}
*/
});
Получение метаданных о полях таблицы: connection.fields(table, callback) возвращает массив с метаданными для каждого поля, в том чисте имя, тип, все модификаторы и флаги, комментарии (подробнее см. пример).
connection.fields('Language', function(err, fields) {
console.dir({fields:fields});
/* Example:
fields: {
LanguageId: {
Field: 'LanguageId',
Type: 'int(10) unsigned',
Collation: null,
Null: 'NO',
Key: 'PRI',
Default: null,
Extra: 'auto_increment',
Privileges: 'select,insert,update,references',
Comment: 'Id(EN),Код(RU)' },
LanguageName: {
Field: 'LanguageName',
Type: 'varchar(32)',
Collation: 'utf8_general_ci',
Null: 'NO',
Key: 'UNI',
Default: null,
Extra: '',
Privileges: 'select,insert,update,references',
Comment: 'Name(EN),Имя(RU)'
}, ...
}
*/
});
Получение списка баз данных доступных по данному соединению: connection.databases(callback) возвращает массив имен баз (или «схем», как их иногда называют).
connection.databases(function(err, databases) {
console.dir({databases:databases});
/* Example:
databases: [ 'information_schema', 'mezha', 'mysql', 'performance_schema', 'test' ]
*/
});
Получение списка таблиц для текущего соединения: connection.tables(callback) возвращает хеш (ассоциативный массив) двойной вложенности, где на первом уровне ключи — имена таблиц, а на втором — метаданные для каждой таблицы.
connection.tables(function(err, tables) {
console.dir({tables:tables});
/* Example:
tables: {
Language: {
TABLE_NAME: 'Language',
TABLE_TYPE: 'BASE TABLE',
ENGINE: 'InnoDB',
VERSION: 10,
ROW_FORMAT: 'Compact',
TABLE_ROWS: 9,
AVG_ROW_LENGTH: 1820,
DATA_LENGTH: 16384,
MAX_DATA_LENGTH: 0,
INDEX_LENGTH: 49152,
DATA_FREE: 8388608,
AUTO_INCREMENT: 10,
CREATE_TIME: Mon Jul 15 2013 03:06:08 GMT+0300 (Финляндия (лето)),
UPDATE_TIME: null,
CHECK_TIME: null,
TABLE_COLLATION: 'utf8_general_ci',
CHECKSUM: null,
CREATE_OPTIONS: '',
TABLE_COMMENT: '_Language:Languages(EN),Языки(RU)'
}, ...
}
*/
});
Получение метаданных указанной таблицы: connection.tableInfo(table, callback) возвращает хеш (ассоциативный массив) с метаданными (подробнее см. в примере).
connection.tableInfo('Language', function(err, info) {
console.dir({tableInfo:info});
/* Example:
tableInfo: {
Name: 'language',
Engine: 'InnoDB',
Version: 10,
Row_format: 'Compact',
Rows: 9,
Avg_row_length: 1820,
Data_length: 16384,
Max_data_length: 0,
Index_length: 49152,
Data_free: 9437184,
Auto_increment: 10,
Create_time: Mon Jul 15 2013 03:06:08 GMT+0300 (Финляндия (лето)),
Update_time: null,
Check_time: null,
Collation: 'utf8_general_ci',
Checksum: null,
Create_options: '',
Comment: ''
}
*/
});
Получение метаданных о всех ключах данной таблицы: connection.indexes(table, callback) возвращает хеш (ассоциативный массив)? ключами первого уровня в нем являются имена ключей базы данных (), а ключами второго уровня — метаданные о каждом ключе (см. пример для подробного списка метаданных).
connection.indexes('Language', function(err, info) {
console.dir({tableInfo:info});
/* Example:
indexes: {
PRIMARY: {
Table: 'language',
Non_unique: 0,
Key_name: 'PRIMARY',
Seq_in_index: 1,
Column_name: 'LanguageId',
Collation: 'A',
Cardinality: 9,
Sub_part: null,
Packed: null,
Null: '',
Index_type: 'BTREE',
Comment: '',
Index_comment: '' },
akLanguage: {
Table: 'language',
Non_unique: 0,
Key_name: 'akLanguage',
Seq_in_index: 1,
Column_name: 'LanguageName',
Collation: 'A',
Cardinality: 9,
Sub_part: null,
Packed: null,
Null: '',
Index_type: 'BTREE',
Comment: '',
Index_comment: ''
}
}
*/
});
Получение процессов на сервере MySQL: connection.processes(callback) возвращает массив хешей, где для каждого процесса даны его параметры.
connection.processes(function(err, processes) {
console.dir({processes:processes});
/* Example:
processes: [ {
ID: 62,
USER: 'mezha',
HOST: 'localhost:14188',
DB: 'mezha',
COMMAND: 'Query',
TIME: 0,
STATE: 'executing',
INFO: 'SELECT * FROM information_schema.PROCESSLIST'
}, {
ID: 33,
USER: 'root',
HOST: 'localhost:39589',
DB: null,
COMMAND: 'Sleep',
TIME: 1,
STATE: '',
INFO: null
} ]
*/
});
Получение глобальных переменных MySQL: connection.globalVariables(callback)
connection.globalVariables(function(err, globalVariables) {
console.dir({globalVariables:globalVariables});
/* Example:
globalVariables: {
MAX_PREPARED_STMT_COUNT: '16382',
MAX_JOIN_SIZE: '18446744073709551615',
HAVE_CRYPT: 'NO',
PERFORMANCE_SCHEMA_EVENTS_WAITS_HISTORY_LONG_SIZE: '10000',
INNODB_VERSION: '5.5.32',
FLUSH_TIME: '1800',
MAX_ERROR_COUNT: '64',
...
}
*/
});
Получение глобального статуса MySQL: connection.globalStatus(callback)
connection.globalStatus(function(err, globalStatus) {
console.dir({globalStatus:globalStatus});
/* Example:
globalStatus: {
ABORTED_CLIENTS: '54',
ABORTED_CONNECTS: '2',
BINLOG_CACHE_DISK_USE: '0',
BINLOG_CACHE_USE: '0',
BINLOG_STMT_CACHE_DISK_USE: '0',
BINLOG_STMT_CACHE_USE: '0',
BYTES_RECEIVED: '654871',
BYTES_SENT: '212454927',
COM_ADMIN_COMMANDS: '594',
...
}
*/
});
Получение списка пользователей MySQL: connection.users(callback)
connection.users(function(err, users) {
console.dir({users:users});
/* Example:
users: [
{
Host: 'localhost',
User: 'root',
Password: '*90E462C37378CED12064BB3388827D2BA3A9B689',
Select_priv: 'Y',
Insert_priv: 'Y',
Update_priv: 'Y',
Delete_priv: 'Y',
Create_priv: 'Y',
Drop_priv: 'Y',
Reload_priv: 'Y',
Shutdown_priv: 'Y',
Process_priv: 'Y',
File_priv: 'Y',
Grant_priv: 'Y',
References_priv: 'Y',
Index_priv: 'Y',
Alter_priv: 'Y',
Show_db_priv: 'Y',
Super_priv: 'Y',
Create_tmp_table_priv: 'Y',
Lock_tables_priv: 'Y',
Execute_priv: 'Y',
Repl_slave_priv: 'Y',
Repl_client_priv: 'Y',
Create_view_priv: 'Y',
Show_view_priv: 'Y',
Create_routine_priv: 'Y',
Alter_routine_priv: 'Y',
Create_user_priv: 'Y',
Event_priv: 'Y',
Trigger_priv: 'Y',
Create_tablespace_priv: 'Y',
ssl_type: '',
ssl_cipher: <Buffer >,
x509_issuer: <Buffer >,
x509_subject: <Buffer >,
max_questions: 0,
max_updates: 0,
max_connections: 0,
max_user_connections: 0,
plugin: '',
authentication_string: ''
}, ...
]
*/
});
Удобства генерации запросов
Ну и совсем уже барские удобства, позволяющие генерировать SQL или полностью или отдельные WHERE выражения. Я сам не сторонник такого сахара, но иногда бывает нужно автоматизировать генерацию запросов, которые заранее не известны и я позволяю себе такую роскошь.
Генерация условий: connection.where(conditions) работает синхронно, а не асинхронно, как другие функции, т.е. не использует callback. Возвращает построенное WHERE выражение SQL для conditions, описанных в стиле JSON. Нужно обязательно смотреть пример для понимания:
var where = connection.where({
id: 5,
year: ">2010",
price: "100..200",
level: "<=3",
sn: "*str?",
label: "str",
code: "(1,2,4,10,11)"
});
console.dir(where);
// Output: "id = 5 AND year > '2010' AND (price BETWEEN '100' AND '200') AND
// level <= '3' AND sn LIKE '%str_' AND label = 'str' AND code IN (1,2,4,10,11)"
Выборка с условием: connection.select(table, whereFilter, callback)
connection.select('Language', '*', { LanguageId: "1..3" }, function(err, results) {
console.dir({select:results});
});
Вставка записи: connection.insert(table, row, callback)
connection.insert('Language', {
LanguageName: 'Tatar',
LanguageSign:'TT',
LanguageISO:'TT',
Caption:'Tatar'
}, function(err, recordId) {
console.dir({insert:recordId});
});
Изменение записи: connection.update(table, row, callback)
connection.update('Language', {
LanguageId: 25,
LanguageName:'Tatarca',
LanguageSign:'TT',
LanguageISO:'TT',
Caption:'Tatarca'
}, function(err, affectedRows) {
console.dir({update:affectedRows});
});
Вставка, если нет такой записи или изменение, если она уже есть: connection.upsert(table, row, callback)
connection.upsert('Language', {
LanguageId: 25,
LanguageName:'Tatarca',
LanguageSign:'TT',
LanguageISO:'TT',
Caption:'Tatarca'
}, function(err, affectedRows) {
console.dir({upsert:affectedRows});
});
Получение количества записей в таблице по заданному фильтру или без фильтра: connection.count(table, whereFilter, callback)
connection.count('Language', { LanguageId: ">3" }, function(err, count) {
console.dir({count:count});
/* Example:
count: 9
*/
});
Удаление записи или нескольких записей: connection.delete(table, whereFilter, callback)
connection.delete('Language', { LanguageSign:'TT' }, function(err, affectedRows) {
console.dir({delete:affectedRows});
});
Варианты использования
1. На Github есть патч к драйверу node-mysql со всеми, приведенными выше функциями: github.com/felixge/node-mysql
2. Но я думаю, что поддерживать его я не стану, и подумываю про то, чтобы сделать отдельную библиотеку с более высоким уровнем абстракции, чем просто драйвер, но не такого высокого уровня как ORM библиотеки. То есть, я не хочу делать из этого громоздкую вещь, а позволить добавлять к соединению через примеси только нужные из трех групп функций. То есть, после создания соединения можно будет выбирать, нужна ли интроспекция, нужны ли простые функции выборки скаляров, массивов и хешей и нужны ли более сложные функции для генерации SQL выражений.
3. И третий вариант использования, это сервер приложений Impress для Node.js, в которые весь этот арсенал встроен. Две группы функций по выборке в Impress есть сразу у всех подключений к MySQL, они примешиваются автоматически, при открытии соединения, и только интроспекцию можно добавить через конфиг где нужно раскомментировать соответствующий плагин, чтобы набор плагинов для MySQL был такой:
plugins: {
require: [
"db",
"db.mysql",
"db.mysql.introspection",
...
]
},...
Тогда плагин автоматически примешается к каждому соединению: if (db.mysql.introspection) db.mysql.introspection(connection);
Тут искодники плагинов к Impress:
- https://github.com/tshemsedinov/impress/blob/master/lib/db.mysql.js
- https://github.com/tshemsedinov/impress/blob/master/lib/db.mysql.introspection.js
А по поводу выпуска этих удобств в качестве отдельной библиотеки, хочу посоветоваться в вами:
Автор: MarcusAurelius