VirtualBox's high CPU bug was still bothering me, I knew I'd have to do something else once my trial licences expired and I'd done the proof of concept for work, so I decided to convert my rig back to LAMP, removing SQL Server from the jAmS setup and replacing it with MySQL.
jAmMy (jQuery - ASP.Net via mono - MySQL) is an even more lightweight technique than JAWS or jAmS and offers the ability to write SQL commands in Javascript and have the results returned as JSON. Obviously this is open to abuse, so it would be advisable not to offer a system that allowed any command to be passed unchecked but for simple queries it would be hard to beat in the performance stakes.
The concept remains the same as before, but each component requires some small changes to replace SQL objects and syntax with their MySQL counterparts.
SQL commands that don't return data need to be executed as NonQuery.
I haven't implemented a wrapper for DELETE due to the inherent danger - the examples below use a stored procedure to perform a specific, limited delete.
The #s are in the parameter string because jQuery needs to pass the JSON parameters as a quoted string within a string, so I used #s to replace quotes within those parameters.
[WebMethod]
//The WebService needs to be declared as [ScriptService]
//to allow the WebMethods to be called from JavaScript
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
//NonQuery methods
public void SQLexecuteNonQuery(string SQLstr)
{
MySqlConnection SQLconn =
new MySqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings[
"MySQLTestDBConnectionString"
].ConnectionString);
MySqlCommand SQLcmd = new MySqlCommand(SQLstr.Replace("#", "'"), SQLconn);
// SqlConnection SQLconn =
// new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings[
// "SQLTestDBConnectionString"
// ].ConnectionString);
// SqlCommand SQLcmd = new SqlCommand(SQLstr.Replace("#", "'"), SQLconn);
SQLconn.Open();
SQLcmd.ExecuteNonQuery();
SQLconn.Close();
}
A wrapper for a SQL INSERT command.
[WebMethod]
[ScriptMethod]
public void SQLinsert(string SQLparm)
{
SQLexecuteNonQuery("INSERT INTO " + SQLparm);
}
A wrapper for a SQL EXEC command. The stored procedure examples I have below do not return useful data, so it uses the NonQuery method.
[WebMethod]
[ScriptMethod]
public void SQLexec(string SQLparm)
{
SQLexecuteNonQuery("CALL " + SQLparm);
//SQLexecuteNonQuery("EXEC " + SQLparm);
}
Performs a SQL command that does return data
Returns a DataTable (presuming the command is valid).
//Query methods
public DataTable SQLexecuteQuery(string SQLstr)
{
MySqlConnection SQLconn =
new MySqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings[
"MySQLTestDBConnectionString"
].ConnectionString);
MySqlCommand SQLcmd = new MySqlCommand(SQLstr.Replace("#", "'"), SQLconn);
// SqlConnection SQLconn =
// new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings[
// "SQLTestDBConnectionString"
// ].ConnectionString);
// SqlCommand SQLcmd = new SqlCommand(SQLstr.Replace("#", "'"), SQLconn); DataSet ds = new DataSet();
DataSet ds = new DataSet();
MySqlDataAdapter da = new MySqlDataAdapter(SQLcmd);
// SqlDataAdapter da = new SqlDataAdapter(SQLcmd);
da.Fill(ds, "reading");
SQLconn.Close();
return ds.Tables[0];
}
Takes a DataTable and converts it to a JSON string - you used to be able to return a List of Dictionary types, but this is no longer allowed,
so a JavaScriptSerializer is used to translate the List into JSON.
I'm only using the data for display purposes, so I avoid the hassle of translating datetime objects by converting each column value to a string (or null).
public string DataTable2JSON(DataTable dt)
{
List<object> RowList = new List<object>();
foreach (DataRow dr in dt.Rows)
{
Dictionary<string, object> ColList =
new Dictionary<string, object>();
foreach (DataColumn dc in dt.Columns)
{
ColList.Add(dc.ColumnName,
(string.Empty == dr[dc].ToString()) ? null : dr[dc].ToString()); ;
}
RowList.Add(ColList);
}
JavaScriptSerializer js = new JavaScriptSerializer();
string JSON = js.Serialize(RowList);
return JSON;
}
Acts as an interface to SQL specifically for SELECT commands - if a WebMethod was offered that allowed free-format SQL commands to be run, your database would not be around for long.
public string SQLselect(string SQLparm)
{
return DataTable2JSON(SQLexecuteQuery("SELECT " + SQLparm));
}
The SQLexec() WebMethod defined in the WebService allows us to call Stored Procedures, so we can go beyond simple SQL commands
and exploit the performance of MySQL, rather than relying on client-side code to manipulate the data.
Note that there are many differences between SQL Server and MySQL syntax when it comes to Stored Procedures.
/*
MySQL version (created in Catalogs section of MySQL Administrator)
*/
CREATE PROCEDURE `SimulatedTestRun`(
runStarted DATETIME,
testPC VARCHAR(50),
testBuild VARCHAR(50),
testCode INT,
outputMatch TINYINT(1))
BEGIN
INSERT INTO TestRun (RunStarted,TestPC,TestBuild)
VALUES (runStarted, testPC, testBuild);
SELECT MAX(RunID) FROM TestRun INTO @LastRunID;
INSERT INTO TestResults (TestCode,RunID,OutputMatch)
VALUES (testCode,@LastRunID,outputMatch);
END
/*
SQL Server version
*/
CREATE PROCEDURE dbo.SimulatedTestRun
/*
Insert a new TestRun record
Use the RunID to create a new TestResults record
*/
@runStarted datetime,
@testPC text,
@testBuild text,
@testCode int,
@outputMatch bit
AS
INSERT INTO TestRun (RunStarted,TestPC,TestBuild)
VALUES (@runStarted, @testPC, @testBuild)
DECLARE @LastRunID int
SELECT @LastRunID = MAX(RunID) FROM TestRun
INSERT INTO TestResults (TestCode,RunID,OutputMatch)
VALUES (@testCode,@LastRunID,@outputMatch)
RETURN
/*
MySQL version
*/
CREATE PROCEDURE `CleanSimulatedTestRun`()
BEGIN
DELETE FROM TestRun WHERE (RunID > 1003);
DELETE FROM TestResults WHERE (TestResultsID > 4);
END
/*
SQL Server version
*/
CREATE PROCEDURE dbo.CleanSimulatedTestRun
/*
Delete latest TestRun and TestResults records
*/
AS
DECLARE @LastRunID int
SELECT @LastRunID = MAX(RunID) FROM TestRun
DELETE FROM TestRun WHERE (RunID = @LastRunID)
DECLARE @LastTestResultsID int
SELECT @LastTestResultsID = MAX(TestResultsID) FROM TestResults
DELETE FROM TestResults WHERE (TestResultsID = @LastTestResultsID)
RETURN
$.ajax() is the best way to handle an ASP.NET WebService within jQuery. The JSON string returned is translated into a JSON object for parsing in the success: callback.
As an example here, a table is constructed using the JSON names as column headers and the values as the table data.
Note here that the only change required is the name of the WebService call.
//_jsSQL() a class to pass messages between Javascript and SQL
//via a WebService
function _jsSQL() {
//_ajaxSQL() is a private method used to pass SQL commands via jQuery's .ajax() method
function _ajaxSQL(_url, _data, _error, _success) {
$.ajax({
type: "POST",
url: "/MySQLService.asmx/" + _url,
data: _data,
contentType: "application/json; charset=utf-8",
dataType: "json",
error: function (msg) {
_error(msg);
},
success: function (msg) {
_success(msg);
}
});
};
//_SQLtable() is a private method to handle requests to turn SQL results into HTML tables
// it allows for an optional callback function when the SQL call is successful
function _SQLtable(_url, _data, _TableName, _fnCallback) {
_ajaxSQL(_url, _data,
function () {
_SQLtableError(_TableName);
},
function(msg) {
_SQLtableSuccess(msg, _TableName, _fnCallback);
});
}
//_SQLtableSuccess is a private method to iterate through the JSON returned from SQL
// and construct an HTML table with a caption and header row based on the table and column names.
// The table is inserted into an HTML element with an ID based on the table name
function _SQLtableSuccess(msg, _TableName, _fnCallback) {
var strJSON = eval(msg.d);
_TableName = _TableName.split(" ")[0];
var JSONtable = "<table><caption>" + _TableName + "</caption>";
JSONtable += "<thead><tr>";
for (var field in strJSON[0]) {
JSONtable += "<th>" + field + "</th>";
}
JSONtable += "</tr></thead>";
for (var i = 0, l = strJSON.length; i < l; i++) {
JSONtable += "<tr>";
var thisRecord = strJSON[i];
for (var field in thisRecord) {
JSONtable += "<td>" + thisRecord[field] + "</td>";
}
JSONtable += "</tr>";
}
JSONtable += "</table>";
$("#" + _TableName).html(JSONtable);
if (_fnCallback) {
_fnCallback();
}
}
//_SQLtableError() replaces the table with an error message if there's a problem
function _SQLtableError(_TableName) {
$("#" + _TableName).text("Sorry - can't fetch " + _TableName + " from the database")
}
//_getTable() creates a table based on the table name with an optional callback
function _getTable(_TableName, _fnCallback) {
_SQLtable(
"SQLselect",
"{SQLparm: '* FROM " + TableName + "'}",
_TableName,
_fnCallback
);
}
//Calling a Stored Procedure is slightly more difficult as we've already used double and single quotes
//in the JSON parameter, so I use #s to define quoted string parameters for the stored procedure:
//_cleanRun() resets and scrolls to the tables displayed on this page
function _cleanRun() {
_ajaxSQL(
"SQLexec",
"{SQLparm: 'CleanSimulatedTestRun'}",
function () {
alert("Can't clean Test Runs");
},
function() { _getTable("TestBed");
_getTable("TestConditions");
_getTable("TestRun ORDER BY RunID",
function () {
$("#TestRun caption").append("<form><input id='btnSTR' type='button' value='Simulate Test Run' /></form>");
});
_getTable("TestResults ORDER BY TestResultsID",
function () { $("html,body").animate({ scrollTop: $("#btnSQL").offset().top }, 500);
$("#lblSQL").fadeIn("slow");
$("#frmSQL").removeClass("loading");
});
}
);
}
//_generateRun() simulates a new test run and updates the tables
function _generateRun(rtRunStarted,rtTestPC,rtTestBuild,rtTestCode,rtOutputMatch) {
_ajaxSQL(
"MYSQLexec",
"{SQLparm: 'SimulatedTestRun #" + rtRunStarted + "#,#" + rtTestPC + "#,#" +
rtTestBuild + "#,#" + rtTestCode + "#,#" + rtOutputMatch + "#'}",
function () {
alert("Can't create new test run");
},
function () {
_getTable("TestRun ORDER BY RunID",
function () {
$("#TestRun caption").append("<form><input id='btnSTR' type='button' value='Simulate Test Run' /></form>");
});
_getTable("TestResults ORDER BY TestResultsID",
function (){
$("#TestRun tr:last td:eq(0)").addClass("highlight");
$("#TestResults tr:last td:eq(0)").addClass("highlight");
$("html,body").animate({ scrollTop: $("#btnSQL").offset().top }, 500);
});
}
);
}
//finally, close out the class by exposing some of the methods publicly
//also return this so calls can be chained
return {
getTable: function (_TableName, _fnCallback) {
_getTable(_TableName, _fnCallback);
return this;
},
cleanRun: function () {
_cleanRun();
return this;
},
generateRun: function (rtRunStarted,rtTestPC,rtTestBuild,rtTestCode,rtOutputMatch) {
_generateRun(rtRunStarted,rtTestPC,rtTestBuild,rtTestCode,rtOutputMatch);
return this;
}
}
};
Then, within our onload function, we can create a _jsSQL() object and call its public methods, e.g.:
var jsSQL = _jsSQL();
jsSQL.cleanRun();
jsSQL.getTable("TestBed")
.getTable("TestConditions")
.getTable("TestRun")
.getTable("TestResults");
jsSQL.generateRun("2009/10/19 6:02:00","TestVista","1.1.0.1",100000,0);
The length of the JSON string returned by the WebService needs to be set to a sensible limit (if you find the default inadequate) - but if you were expecting more than a screenful of results, you would page it, wouldn't you?...
I've seen worse. Besides, this is an 8 year old 32-bit desktop with 1GB of RAM running 3 websites and a mailserver off Apache and IIS on 2003 Server and SQL Server 2008 inside VirtualBox simultaneously, so it ain't doing badly.
Setting up this rig with a Windows Server and SQL Server inside a virtual machine and playing nice with Apache on Linux was not easy, believe me...
Robert Robbins at williamsportwebdeveloper.com
for the WebService.
Dave Ward at encosia.com
for the jQuery.
Ubuntu Community docs for mod_mono installation instructions
Jonathan Pryor for the hints on connecting to SQL with mono.
David Longnecker at Ramblings of the Sleepy for the skinny on getting permissions for a SQL authenticated account to run stored procedures.
Nobody for info on converting SQL Server Stored Procedures to MySQL - why is that info so hard to find? I just used trial and error.
These examples are based on this skeleton outline of some sample tables and relationships: