2016-09-06 60 views
10

由于内联mysql查询,我们面临代码质量问题。具有自编写的MySQL查询确实杂波的代码,同时也增加了代码库等何处存储执行的SQL命令

我们的代码是堆满了东西一样

/* 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过程存储在一个地方?作为附加信息,我们使用节点(〜0.12)并表示暴露一个API,访问MySQL数据库。

我已经想过,使用JSON会导致更大的混乱。再加上它可能不可能,因为JSON的字符集有点严格,JSON可能也不会喜欢多行字符串。

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

任何帮助表示赞赏,我不能成为这个星球上解决这个问题的第一个,所以也许有人有一个工作,很好的解决方案! PS:我尝试使用像squel这样的库,但这并没有真正的帮助,因为我们的查询是复杂的,你可以看到。这主要是为了让我们的查询进入“查询中心”。

+0

我可以知道为什么你不能去ES6,因为我发现它不会影响你现有的代码?你只需要使用一个转译器。 –

+0

目前的依赖关系太多了,至少这就是我所说的:S我也尝试去使用节点6.5,但应用程序在启动时崩溃。目前我尝试将SQL命令放到中央。如果node6.x是一个可能的问题解决者,我将不得不提出它并让它运行。 – m4mbax

+0

你可以考虑在数据库中引入视图来简化一些'SELECT'查询(比如你的例子)。并且可能还有存储过程来简化以某种方式更新数据的查询。当然,这种方法有优点和缺点 - 视图/存储过程必须维护,有时会存在增加隐含的风险(例如,包含其他视图的视图 - 可能需要深入钻研才能找出实际的表格是)。但它可能很有用,例如当一次又一次使用相同连接或'WHERE'条件时。 –

回答

11

我更喜欢把每个更大的查询放在一个文件中。通过这种方式,您可以使用语法高亮显示,并且在服务器启动时很容易加载。为了构造这个,我通常有一个文件夹用于所有查询,并且每个模型都有一个文件夹。

# queries/mymodel/select.mymodel.sql 
SELECT * FROM mymodel; 

// in mymodel.js 
const fs = require('fs'); 
const queries = { 
    select: fs.readFileSync(__dirname + '/queries/mymodel/select.mymodel.sql', 'utf8') 
}; 
+0

这是高性能吗?它是否在启动时将所有查询都读取到内存中?还是在读取queries.elect时读取? – m4mbax

+1

它应该是相当高性能的,因为它只读取一次查询(在serverstart上)。要插入我通常放置的数据?和??在查询中,这是mysql模块所能理解的。所以是的,它把它们保存在内存中 –

+0

由于所有其他答案或多或少都是相同的方法,但不同的实现我接受了这个答案。一旦我们发布了我们的当前版本,我将确实开发自己的模块。也许,我希望,如果它能够像你们所说的那样工作,我会把它贡献给GH。谢谢任何人回答你的答案,真的很感激它。如果我有一天把它放到一个模块中,我会回来用我的模块回答我自己的问题。让我知道是否有人愿意贡献! – m4mbax

5

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

create procedure sp_query() 
select * from table1; 
+0

我同意Pavel – Robin

2

您可以创建一个查看哪个查询。

则从查看

选择我没有看到在查询任何参数,所以我想创建视图是可能的。

2

为所有查询创建存储过程,并替换调用诸如var sql = "CALL usp_get_packages"之类的过程的变量sql = "SELECT..."

这是最好的性能和应用程序没有依赖关系中断。取决于查询的数量可能是一项艰巨的任务,但对于每个方面(可维护性,性能,依赖性等)来说都是最好的解决方案。

3

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

我们在MySQL中创建了一个表格,允许保存Template_Name(unique),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和谁的时间。

从性能的角度来看,这将以即时方式工作,并且当您在添加新模板时依赖于启动服务器进程时,不必读取任何文件或重新启动服务器。

3

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

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

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

下面的查询可以在/查询/复杂的目录下,生活在自己的文件,该文件将有一个描述性的名称(假设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 

做这样的事情,你将字符串连接的所有噪声移动到另一个地方你会期望,你将能够在一个地方很容易地找到你所有复杂的查询。

3

有几件事情你想要做。首先,你想存储多线没有ES6。您可以利用功能的toString

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));

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

//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.

可以require必要的软件包,并建立你的逻辑这个模块权或建立更好的面向对象设计一个通用的封装模块。

//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) { 
    ... 
    }); 
3

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

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

一个简单的技巧(事实上,我发现它比使用“+”连接字符串的效率惊人得多)是为每行使用字符串数组并加入它们。

它仍然是一团糟,但至少对我来说有点清楚(特别是当我使用“\ n”作为分隔符而不是空格时,为了调试打印出来的结果字符串更具可读性) 。

例子:

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。它可能不适用于太复杂的查询,但是,如果您有提供参数可能会有所不同的情况,则通过完全删除不需要的查询部分来避免(也是次级)“聚合”混淆非常有帮助。它也在GitHub,(这不是太复杂的代码),所以你可以扩展它,如果你觉得它有用。

如果你喜欢单独的文件:

关于具有单个或多个文件,有多个文件是从视图的阅读效率(更多的文件打开/关闭开销和难OS级缓存)点效率较低。但是,如果您在启动时单次加载所有这些数据,则实际上并不会有明显差异。

所以,唯一的缺点就是对查询集合进行“全局浏览”太困难了。即使如果您有大量的查询,我认为最好将这两种方法混合使用。也就是说:在同一个文件中分组相关查询,以便每个模块,子模型或您选择的任何标准都有单个文件。

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

当然:如果你还没有使用vim(真正的??),你不会有这个选项,但是确定在你的编辑器中有另一种选择。如果不是,你总是可以使用语法折叠和“function(my_tag){”作为标记。

例如:

---(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)------------------------------------------------ 

其中,使用适当选择的标签,更加方便管理,并从解析的角度来看,并不难分析整个文件拆分查询的分隔行,并使用标签作为索引来查询索引。

肮脏的例子:

#!/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) 

最后(想法),如果你做尽可能多的努力,不会是一个坏主意,每个查询的标签告诉如果查询是一起添加一些布尔标志打算频繁使用或仅偶尔使用。然后,您可以使用该信息在应用程序启动时准备好这些语句,或者仅在它们将被使用超过一次时使用。

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

产生一个对象
0

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

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

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 = ` 
[email protected]@[email protected]@ 
    select foo from bar 
    where x = ${someVar} and y = ${someOtherVar} 
[email protected]@/[email protected]@ 
`; 

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

这种方法的优点是:

  • 可读性很强,即使是一些JavaScript的开销。

  • 参数被放置为可读的变量名称而不是愚蠢的“$ 1,$ 2”等等,并且在文件顶部显式声明,因此检查它们必须提供的顺序很简单。

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

  • 但是,也可以与node path/to/myQuery.sql.js获得有效的SQL来在SQL解释

    • 这样就可以避免来回拷贝的混乱和背面的查询和替换的参数规格来执行直接执行(或值)每次从查询测试环境到应用程序代码:只需使用相同的文件。

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

实施例:

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

注: '@@ SQL @@' 和 '@@/SQL @@'(或相似的)标签是完全可选的,但很at least in Vim

事实上,我其实并没有直接写下(...) | psql...的代码到控制台,而是简单地(在一个vim缓冲区中):

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

...多次测试条件我想测试,并通过肉眼选择所需的模块并输入执行它们:!bash | psql ...

+1

其实,非常感谢你的意见。当使用较短的SQL片段时,我们正在从“巨大的”SQL转换到DAO,所写的内容非常方便。德福会牢记这一点! – m4mbax

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')); 

下面是它在行动片段(使用模板字符串,而不是一个文件):

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

 
const queries = jsyaml.load(yml); 
 
console.dir(queries); 
 
console.log(queries.someQuery);
<script src="https://unpkg.com/[email protected]/dist/js-yaml.min.js"></script>