在哪里存储用于执行的SQL命令

时间:2016-09-06 07:32:57

标签: mysql node.js

由于内联mysql查询,我们面临代码质量问题。使用自编写的mysql查询会使代码混乱并增加代码库等。

我们的代码混杂着像

这样的东西
/* beautify ignore:start */
/* jshint ignore:start */
var sql = "SELECT *"
+" ,DATE_ADD(sc.created_at,INTERVAL 14 DAY) AS duedate"
+" ,distance_mail(?,?,lat,lon) as distance,count(pks.skill_id) c1"
+" ,count(ps.profile_id) c2"
+" FROM TABLE sc"
+" JOIN "
+" PACKAGE_V psc on sc.id = psc.s_id "
+" JOIN "
+" PACKAGE_SKILL pks on pks.package_id = psc.package_id  "
+" LEFT JOIN PROFILE_SKILL ps on ps.skill_id = pks.skill_id and         ps.profile_id = ?"
+" WHERE sc.type in "
+" ('a',"
+" 'b',"
+" 'c' ,"
+" 'd',"
+" 'e',"
+" 'f',"
+" 'g',"
+" 'h')"
+" AND sc.status = 'open'"
+" AND sc.crowd_type = ?"
+" AND sc.created_at < DATE_SUB(NOW(),INTERVAL 10 MINUTE) "
+" AND sc.created_at > DATE_SUB(NOW(),INTERVAL 14 DAY)"
+" AND distance_mail(?, ?,lat,lon) < 500"
+" GROUP BY sc.id"
+" HAVING c1 = c2 "
+" ORDER BY distance;";
/* jshint ignore:end */
/* beautify ignore:end */

我不得不模糊一点代码。

正如您所看到的,在您的代码中重复使用它是不可读的。另外因为atm我们不能去ES6,因为多行字符串,这至少会使字符串变得漂亮。

现在的问题是,有没有办法将SQL程序存储在一个地方?作为附加信息,我们使用node(~0.12)和express来公开API,访问MySQL数据库。

我已经考虑过,使用JSON,这将导致更大的混乱。此外,它甚至可能是不可能的,因为JSON的字符集有点严格,JSON可能也不喜欢有多行字符串。

然后我提出了将SQL存储在文件中并在启动节点应用程序时加载的想法。现在,这是我在一个地方获取SQL查询并将其提供给其余节点模块的最好机会。 这里的问题是,使用ONE文件?每个查询使用一个文件?每个数据库表使用一个文件?

任何帮助都表示赞赏,我不能成为这个星球上第一个解决这个问题的人,所以也许有人有一个有效的解决方案!

PS:我尝试使用像squel这样的库,但这并没有什么帮助,因为我们的查询很复杂,你可以看到。它主要是将我们的查询变成“查询中心”。

12 个答案:

答案 0 :(得分:11)

我更喜欢将每个更大的查询放在一个文件中。这样,您可以使用语法突出显示,并且可以在服务器启动时轻松加载。为了构建这个,我通常有一个文件夹用于所有查询,并在每个模型的一个文件夹内。

#<Mysql2::Error: Lost connection to MySQL server at 'reading initial communication packet', system error: 0>

答案 1 :(得分:5)

在需要时将查询放入代码中的数据库过程和调用过程。

create procedure sp_query()
select * from table1;

答案 2 :(得分:5)

我建议您将查询存储在远离js代码的.sql文件中。这将分离关注点并使代码和查询更具可读性。根据您的业务,您应该具有嵌套结构的不同目录。

例如:

queries
├── global.sql
├── products
│   └── select.sql
└── users
    └── select.sql

现在,您只需在应用程序启动时需要所有这些文件。您可以手动执行也可以使用某些逻辑。下面的代码将读取所有文件(同步)并生成与上面的文件夹具有相同层次结构的对象

var glob = require('glob')
var _  = require('lodash')
var fs = require('fs')

// directory containing all queries (in nested folders)
var queriesDirectory = 'queries'

// get all sql files in dir and sub dirs
var files = glob.sync(queriesDirectory + '/**/*.sql', {})

// create object to store all queries
var queries = {}

_.each(files, function(file){
    // 1. read file text
    var queryText = fs.readFileSync(__dirname + '/' + file, 'utf8')

    // 2. store into object
    // create regex for directory name
    var directoryNameReg = new RegExp("^" + queriesDirectory + "/")

    // get the property path to set in the final object, eg: model.queryName
    var queryPath = file
        // remove directory name
        .replace(directoryNameReg,'')
        // remove extension
        .replace(/\.sql/,'')
        // replace '/' with '.'
        .replace(/\//g, '.')

    //  use lodash to set the nested properties
    _.set(queries, queryPath, queryText)
})

// final object with all queries according to nested folder structure
console.log(queries)

日志输出

{
    global: '-- global query if needed\n',
    products: {
        select: 'select * from products\n'
    },

    users: {
        select: 'select * from users\n'
    }
}

因此您可以访问所有查询,例如queries.users.select

答案 3 :(得分:3)

我来自不同的平台,所以我不确定这是不是你想要的。像你的应用程序一样,我们有很多模板查询,我们不喜欢在应用程序中进行硬编码。

我们在MySQL中创建了一个表,允许保存Template_Name(唯一),Template_SQL。

然后我们在应用程序中编写了一个返回SQL模板的小函数。 像这样的东西:

SQL = fn_get_template_sql(Template_name);

然后我们处理这样的SQL: 伪:

if SQL is not empty
    SQL = replace all parameters// use escape mysql strings from your parameter
    execute the SQL

或者您可以使用最安全的方式阅读SQL,创建连接并添加参数。

这允许您随时随地编辑模板查询。您可以为模板表创建一个审计表,捕获所有先前的更改,以便在需要时还原为以前的模板。您可以扩展表并捕获上次编辑SQL的人员和时间。

从性能的角度来看,这可以像在运行中一样工作,当您在添加新模板时依赖于启动服务器进程时,您不必读取任何文件或重新启动服务器。

答案 4 :(得分:3)

您可以创建一个全新的npm模块让我们假设自定义查询模块并将所有复杂查询放在那里。

然后,您可以按资源和操作对所有查询进行分类。例如,dir结构可以是:

/index.js -> it will bootstrap all the resources
/queries
/queries/sc (random name)
/queries/psc (random name)
/queries/complex (random name)

以下查询可以存在于其自己的文件中的/ queries / complex目录下,并且该文件将具有描述性名称(假设为retrieveDistance)

// You can define some placeholders within this var because possibly you would like to be a bit configurable and reuseable in different parts of your code.
/* jshint ignore:start */
var sql = "SELECT *"
+" ,DATE_ADD(sc.created_at,INTERVAL 14 DAY) AS duedate"
+" ,distance_mail(?,?,lat,lon) as distance,count(pks.skill_id) c1"
+" ,count(ps.profile_id) c2"
+" FROM TABLE sc"
+" JOIN "
+" PACKAGE_V psc on sc.id = psc.s_id "
+" JOIN "
+" PACKAGE_SKILL pks on pks.package_id = psc.package_id  "
+" LEFT JOIN PROFILE_SKILL ps on ps.skill_id = pks.skill_id and ps.profile_id = ?"
+" WHERE sc.type in "
+" ('a',"
+" 'b',"
+" 'c' ,"
+" 'd',"
+" 'e',"
+" 'f',"
+" 'g',"
+" 'h')"
+" AND sc.status = 'open'"
+" AND sc.crowd_type = ?"
+" AND sc.created_at < DATE_SUB(NOW(),INTERVAL 10 MINUTE) "
+" AND sc.created_at > DATE_SUB(NOW(),INTERVAL 14 DAY)"
+" AND distance_mail(?, ?,lat,lon) < 500"
+" GROUP BY sc.id"
+" HAVING c1 = c2 "
+" ORDER BY distance;";
/* jshint ignore:end */

module.exports = sql;

顶级index.js将导出包含所有复杂查询的对象。一个例子可以是:

var sc = require('./queries/sc');
var psc = require('./queries/psc');
var complex = require('./queries/complex');

// Quite important because you want to ensure that no one will touch the queries outside of
// the scope of this module. Be careful, because the Object.freeze is freezing only the top
// level elements of the object and it is not recursively freezing the nested objects.
var queries = Object.freeze({
  sc: sc,
  psc: psc,
  complex: complex
});

module.exports = queries;

最后,在您的主代码上,您可以使用以下模块:

var cq = require('custom-queries');
var retrieveDistanceQuery = cq.complex.retrieveDistance;
// @todo: replace the placeholders if they exist

执行类似的操作,您可以将字符串连接的所有噪音移动到您期望的另一个地方,并且您将能够在一个地方轻松找到所有复杂的查询。

答案 5 :(得分:3)

您想要做一些事情。首先,您想要存储没有ES6的多行。您可以利用函数的toString

&#13;
&#13;
var getComment = function(fx) {
        var str = fx.toString();
        return str.substring(str.indexOf('/*') + 2, str.indexOf('*/'));
      },
      queryA = function() {
        /* 
            select blah
              from tableA
             where whatever = condition
        */
      }

    console.log(getComment(queryA));
&#13;
&#13;
&#13;

您现在可以创建一个模块并存储大量这些功能。例如:

&#13;
&#13;
//Name it something like salesQry.js under the root directory of your node project.
var getComment = function(fx) {
    var str = fx.toString();
    return str.substring(str.indexOf('/*') + 2, str.indexOf('*/'));
  },
  query = {};

query.template = getComment(function() { /*Put query here*/ });
query.b = getComment(function() {
  /*
  SELECT *
   ,DATE_ADD(sc.created_at,INTERVAL 14 DAY) AS duedate
   ,distance_mail(?,?,lat,lon) as distance,count(pks.skill_id) c1
   ,count(ps.profile_id) c2
    FROM TABLE sc
    JOIN  PACKAGE_V psc on sc.id = psc.s_id 
    JOIN  PACKAGE_SKILL pks on pks.package_id = psc.package_id  
    LEFT JOIN PROFILE_SKILL ps on ps.skill_id = pks.skill_id AND ps.profile_id = ?
   WHERE sc.type in ('a','b','c','d','e','f','g','h')
     AND sc.status = 'open'
     AND sc.crowd_type = ?
     AND sc.created_at < DATE_SUB(NOW(),INTERVAL 10 MINUTE) 
     AND sc.created_at > DATE_SUB(NOW(),INTERVAL 14 DAY)
     AND distance_mail(?, ?,lat,lon) < 500
   GROUP BY sc.id
  HAVING c1 = c2 
  ORDER BY distance;
  */
});

//Debug
console.log(query.template);
console.log(query.b);

//module.exports.query = query //Uncomment this.
&#13;
&#13;
&#13;

您可以require必要的软件包并在此模块中构建您的逻辑,或构建一个通用的包装模块以实现更好的OO设计。

//Name it something like SQL.js. in the root directory of your node project.
var mysql = require('mysql'),
  connection = mysql.createConnection({
    host: 'localhost',
    user: 'me',
    password: 'secret',
    database: 'my_db'
  });

module.exports.load = function(moduleName) {
  var SQL = require(moduleName);
  return {
    query: function(statement, param, callback) {
      connection.connect();
      connection.query(SQL[statement], param, function(err, results) {
        connection.end();
        callback(err, result);
      });
    }
  });

要使用它,您可以执行以下操作:

var Sql = require ('./SQL.js').load('./SalesQry.js');

Sql.query('b', param, function (err, results) {
  ...
  });

答案 6 :(得分:3)

这无疑是一个百万美元的问题,我认为正确的解决方案总是取决于案例。

这是我的想法。希望可以帮助:

一个简单的技巧(事实上,我读到它比使用&#34; +&#34;加入字符串更有效率)是为每一行使用字符串数组并加入它们。

它仍然是一团糟,但至少对我而言,更清晰一点(特别是当我使用时,&#34; \ n&#34;作为分隔符而不是空格,以使打印后的结果字符串更具可读性出去调试)。

示例:

var sql = [
    "select foo.bar",
    "from baz",
    "join foo on (",
    "  foo.bazId = baz.id",
    ")", // I always leave the last comma to avoid errors on possible query grow.
].join("\n"); // or .join(" ") if you prefer.
  

作为提示,我在自己的SQL "building" library中使用该语法。它可能无法在过于复杂的查询中起作用,但是,如果您遇到的参数可能会有所不同,那么避免(也是次要的)&#34;合并&#34;完全删除不需要的查询部分。它也在GitHub上,(并且它的代码太复杂了),所以如果你认为它有用,你可以扩展它。

如果您更喜欢单独的文件:

关于拥有单个或多个文件,从读取效率的角度来看,拥有多个文件的效率较低(文件打开/关闭开销更多,操作系统级别缓存更难)。但是,如果你在启动时单次加载所有这些,那实际上并不是一个难以察觉的区别。

所以,唯一的缺点(对我来说)是太难以全局一瞥&#34;您的查询集合。即使如此,如果您有大量的查询,我认为最好将这两种方法混合使用。即:在同一文件中对相关查询进行分组,以便每个模块,子模型或您选择的任何条件都有单个文件。

当然:单个文件会导致相对&#34;巨大的&#34;文件,也很难处理&#34;起初&#34;。但我(几乎)使用vim基于标记的折叠(foldmethod=marker),这对处理这些文件非常有帮助。

  

当然:如果你还没有(还)使用vim(真的??),你就不会有这个选项,但你的编辑器中确实有另一种选择。如果没有,你总是可以使用语法折叠和类似&#34; function(my_tag){&#34;作为标记。

例如:

---(Query 1)---------------------/*{{{*/
select foo from bar;
---------------------------------/*}}}*/

---(Query 2)---------------------/*{{{*/
select foo.baz 
from foo
join bar using (foobar)
---------------------------------/*}}}*/

...折叠后,我将其视为:

+--  3 línies: ---(Query 1)------------------------------------------------

+--  5 línies: ---(Query 2)------------------------------------------------

使用正确选择的标签,管理起来要方便得多,从解析的角度来看,通过分离行解析整个文件拆分查询并使用标签作为键来索引查询并不困难。 / p>

肮脏的例子:

#!/usr/bin/env node
"use strict";

var Fs = require("fs");

var src = Fs.readFileSync("./test.sql");

var queries = {};


var label = false;

String(src).split("\n").map(function(row){
    var m = row.match(/^-+\((.*?)\)-+[/*{]*$/);
    if (m) return queries[label = m[1].replace(" ", "_").toLowerCase()] = "";
    if(row.match(/^-+[/*}]*$/)) return label = false;
    if (label) queries[label] += row+"\n";
});

console.log(queries);
// { query_1: 'select foo from bar;\n',
//   query_2: 'select foo.baz \nfrom foo\njoin bar using (foobar)\n' }

console.log(queries["query_1"]);
// select foo from bar;

console.log(queries["query_2"]);
// select foo.baz
// from foo
// join bar using (foobar)
  

最后(想法),如果你做了很多努力,添加一些布尔标记与每个查询标签一起来告诉这个查询是打算经常使用还是偶尔使用都不是一个坏主意。然后,您可以使用该信息在应用程序启动时或仅在多次使用它们时准备这些语句。

答案 7 :(得分:2)

您可以创建该查询的视图吗?

然后从视图中选择

我在查询中看不到任何参数,所以我认为可以创建视图。

答案 8 :(得分:2)

为所有查询创建商店过程,并替换var Startup以调用var services.Configure<ForwardedHeadersOptions>(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedProto; }); 等过程。

这是性能最佳,并且应用程序没有依赖性中断。根据查询的数量可能是一项艰巨的任务,但对于每个方面(可维护性,性能,依赖性等)都是最佳解决方案。

答案 9 :(得分:1)

使用ES6字符串模板使用单独文件的另一种方法。

  

当然,这并不能回答原始问题,因为它需要ES6,但已经有一个我不想替换的接受答案。我只是认为从讨论存储和管理备选方案的角度来看它很有意思。

// myQuery.sql.js
"use strict";

var p = module.parent;
var someVar = p ? '$1' : ':someVar'; // Comments if needed...
var someOtherVar = p ? '$2' : ':someOtherVar';

module.exports = `
--@@sql@@
    select foo from bar
    where x = ${someVar} and y = ${someOtherVar}
--@@/sql@@
`;

module.parent || console.log(module.exports);
// (or simply "p || console.log(module.exports);")
  

注意:这是原始(基本)方法。一世   后来发展它增加了一些有趣的改进   ( BONUS BONUS 2 FINAL EDIT 部分)。见底部   这篇文章的全功能snipet。

此方法的优点

  • 非常易读,即使是小小的javascript开销。

    • 它也可以是正确的语法高亮(at least in Vim)javascript和SQL部分。
  • 参数被放置为可读变量名而不是愚蠢的&#34; $ 1,$ 2&#34;等...并在文件顶部显式声明,因此检查起来很简单必须按顺序提供。

  • 可以要求myQuery = require("path/to/myQuery.sql.js")获取有效的查询字符串,其中包含$ 1,$ 2等...指定顺序的位置参数。

  • 但是,也可以用node path/to/myQuery.sql.js直接执行,获取在sql解释器中执行的有效SQL

    • 通过这种方式,您可以避免每次从查询测试环境到应用程序代码时复制查询和替换参数规范(或值)的混乱:只需使用相同的文件。

    • 注意:我将PostgreSQL语法用于变量名称。但是对于其他数据库,如果不同,它很容易适应。

  • 更重要的是:通过更多调整(参见 BONUS 部分),您可以在可行的控制台测试工具中将其转换为:

    • 通过执行node myQueryFile.sql.js parameter1 parameter2 [...]
    • 之类的操作生成参数化的sql
    • ...或通过管道直接执行它到您的数据库控制台。例如:node myQueryFile.sql.js some_parameter | psql -U myUser -h db_host db_name
  • 更多:您还可以调整查询,使其在从控制台执行时表现略有不同(请参阅 BONUS 2 部分),避免浪费空间显示大而无意义的数据同时保留当需要它的应用程序读取查询时。

    • 当然:您可以将其再次传送到less -S以避免换行,并且可以通过在水平和垂直方向上滚动来轻松浏览数据。

示例:

(
    echo "\set someVar 3"
    echo "\set someOtherVar 'foo'"
    node path/to/myQuery.sql.js
) | psql dbName
  

备注:

     
      
  • &#39; @@ SQL @@&#39;和&#39; @@ / sql @@&#39; (或类似的)标签是完全可选的,   但对于正确的语法高亮显示非常有用,at least in Vim

  •   
  • 此额外管道不再需要(参见 BONUS 部分)。

  •   

实际上,我实际上并没有将(...) | psql...代码直接写入控制台,而只是(在vim缓冲区中):

echo "\set someVar 3"
echo "\set someOtherVar 'foo'"
node path/to/myQuery.sql.js

...我想通过直观地选择所需的块并输入:!bash | psql ...

来测试和执行测试条件的次数

BONUS:(编辑)

我最终在许多项目中使用这种方法,只需要进行一些简单的修改即可改变最后一行:

module.parent || console.log(module.exports);
// (or simply "p || console.log(module.exports);")

...由:

p || console.log(
`
\\set someVar '''${process.argv[2]}'''
\\set someOtherVar '''${process.argv[3]}'''
`
+ module.exports
);

这样我只需通过将参数作为位置参数传递,就可以从命令行生成参数化的查询。例如:

myUser@myHost:~$ node myQuery.sql.js foo bar

\set someVar '''foo'''
\set someOtherVar '''bar'''

--@@sql@@
    select foo from bar
    where x = ${someVar} and y = ${someOtherVar}
--@@/sql@@

...而且,比那更好:我可以将它传递给postgres(或任何其他数据库)控制台,就像这样:

myUser@myHost:~$ node myQuery.sql.js foo bar | psql -h dbHost -u dbUser dbName
 foo  
------
  100
  200
  300
(3 rows)

这种方法可以更轻松地测试多个值,因为您可以简单地使用命令行历史记录来恢复以前的命令,只需编辑您想要的任何内容。

奖励2:

两个小技巧:

1。有时我们需要检索一些包含二进制和/或大数据的列,这些列很难从控制台读取,事实上,我们甚至不需要查看它们在测试查询时完全没有。

在这种情况下,我们可以利用p变量的优势来改变查询的输出并缩短,更正确地格式化,或者只是从投影中删除该列。

示例:

  • 格式:${p ? jsonb_column : "jsonb_pretty("+jsonb_column+")"},

  • 缩短:${p ? long_text : "substring("+long_text+")"},

  • 删除:${p ? binary_data + "," : ""(请注意,在这种情况下,我在exprssion中移动了逗号,因为它可以在控制台版本中避免使用。

2。实际上并不是一招,只是提醒一下:我们都知道要在控制台中处理大输出,我们只需要将它传递给less命令。 / p>

但是,至少我,经常原谅,当输出是表格对齐且太宽而不适合我们的终端时,有-S修饰符指示较少不要换行而是让我们滚动文本在水平方向上探索数据。

此处应用此更改剪辑的原始完整版本:

// myQuery.sql.js
"use strict";

var p = module.parent;
var someVar = p ? '$1' : ':someVar'; // Comments if needed...
var someOtherVar = p ? '$2' : ':someOtherVar';

module.exports = `
--@@sql@@
    select
        foo
        , bar
        , ${p ? baz : "jsonb_pretty("+baz+")"}
        ${p ? ", " + long_hash : ""}
    from bar
    where x = ${someVar} and y = ${someOtherVar}
--@@/sql@@
`;

p || console.log(
`
\\set someVar '''${process.argv[2]}'''
\\set someOtherVar '''${process.argv[3]}'''
`
+ module.exports
);

最终编辑:

我一直在进化这个概念,直到它变得太宽而不能严格手动处理。

最后,利用优秀的ES6 + 标记模板,我实现了一种更简单的库驱动方法。

所以,如果有人对它感兴趣,那么它是:SQLTT

答案 10 :(得分:1)

我迟到了,但是如果你想将相关的查询存储在一个文件中,YAML非常适合,因为它比任何其他数据序列化格式更好地处理任意空格,并且它有一些其他很好的功能,如评论:

someQuery: |-
  SELECT *
   ,DATE_ADD(sc.created_at,INTERVAL 14 DAY) AS duedate
   ,distance_mail(?,?,lat,lon) as distance,count(pks.skill_id) c1
   ,count(ps.profile_id) c2
    FROM TABLE sc
    -- ...

# Here's a comment explaining the following query
someOtherQuery: |-
  SELECT 1;

这样,使用像js-yaml之类的模块,您可以在启动时轻松地将所有查询加载到对象中,并通过合理的名称访问每个查询:

const fs = require('fs');
const jsyaml = require('js-yaml');
export default jsyaml.load(fs.readFileSync('queries.yml'));

以下是其中的一小部分(使用模板字符串而不是文件):

&#13;
&#13;
const yml =
`someQuery: |-
  SELECT *
    FROM TABLE sc;
someOtherQuery: |-
  SELECT 1;`;

const queries = jsyaml.load(yml);
console.dir(queries);
console.log(queries.someQuery);
&#13;
<script src="https://unpkg.com/js-yaml@3.8.1/dist/js-yaml.min.js"></script>
&#13;
&#13;
&#13;

答案 11 :(得分:0)

将查询放入db过程后,在代码中调用过程。 @paval也已经回答了 您也可以参考here

          创建过程sp_query()
           select * from table1;