2016-09-06 59 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>