65.9K
CodeProject 正在变化。 阅读更多。
Home

MySQL-Fullltext:使用 MySQL 的简单搜索引擎的开始

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (7投票s)

2013年7月27日

CPOL

16分钟阅读

viewsIcon

34871

downloadIcon

898

MySQL-Fullltext:使用MySQL C-API、WCF和jQuery构建一个简单的搜索引擎

引言

本文介绍了使用C语言实现一个简单搜索引擎的入门知识。

2013年8月22日发布的第一个版本仅执行查询。新版本实现了CRUD功能,这意味着您可以创建新记录、读取记录、更新记录和删除记录。新版本利用了jQuery插件 DataTableseditable。代码版本1已进行了清理,并且更好地处理了null值。

我通常使用SQL Server和C#。但我教C/C++编程的朋友希望远离微软。过去,MySQL对我来说不是一个好的数据库,因为它在标准安装中缺乏事务支持,但它正在变得成熟。我使用MySQL 5.6 64位版,以InnoDB作为数据库,并使用Unicode(utf8)编码,这是新数据库的默认设置。

Freetext是InnoDB的一个新功能,它首次在MySQL 5.6版本中引入。

我通常更喜欢C++而不是C,即使是小型项目:不需要记住所有函数名,而是拥有一些带有所需操作的类,并且具有良好的Intellisense支持,这很好。在C++中,您也有用于集合和字符串的STL或Boost。

MySQL的C++接口相当小,而C接口却非常成熟,所以我决定使用C接口。

C DLL通过WCF发布以支持Ajax请求。在Visual Studio Ultimate 2012中,我使用了C#的"WCF服务应用程序"模板。我搜索了在C++中托管WebService的方法,但只找到了一些调用托管C++中WebService的示例。

仅用于查询的UI是一个HTML页面,使用了JQuery和Jquery-UI的autocomplete功能。该页面被添加到"WCF服务应用程序"的网站中,项目名为VisionWeb。页面名为"DefaultQueryOnly.html"。

网页看起来是这样的

Search Page

对于版本2,我使用了文件"default.html"。页面看起来是这样的

Search Page version 2

我将项目配置为框架4.0、64位和调试。如果您使用32位MySQL服务器,则需要更改此设置。您必须将UNICODE选项保留在其默认值true。

配置MySQL

您可能会打开VisionDAL项目,它位于VisionSmall解决方案中。可能需要修改MySQL C接口的链接。这里,我将描述如何在新建项目中设置MySQL接口:检查这些设置是否正确,特别是mysql.lib文件和VisionDAL.dll的路径。

在Visual Studio中,添加一个名为VisionDAL的项目,使用模板"其他语言/Visual C++/空项目"。在向导中,您只需要将"**应用程序类型**"更改为DLL。将VisionDAL.cpp重命名为VisionDAL.c,这足以让Visual Studio从编译C++切换到C。向项目中添加一个名为"VisionDAL.h"的头文件。

在解决方案资源管理器中,右键单击VisionDAL项目并选择**属性**。在"配置属性"/链接器/输入下,选择"**其他依赖项**",然后添加libmysql.lib到路径,不要忘记分隔符";"。

在"配置属性"/链接器/常规下,选择"**其他库目录**",对我来说,添加C:\Program Files\MySQL\MySQL Server 5.6\lib>。现在我们已经链接了C接口,但实现libmysql.lib中调用的DLL必须位于可执行文件的系统搜索路径中:从控制面板选择系统,点击"**高级系统设置**","**点击环境变量**",在"**系统变量**"下选择Path,然后添加libmysql.lib的路径(DLL与lib文件在同一个文件夹中):;C:\Program Files\MySQL\MySQL Server 5.6\lib

我们还需要将VisionDal.dll也添加到我们的Path中,IIS在您将DLL放在其网站的bin文件夹中时找不到它。将<解决方案路径>/x64/debug添加到Path。我需要重启才能使此设置生效,最终通过切换"**托管管道模式**"设置来重启应用程序池可能会有帮助 - 您可以在IIS管理器中执行此操作。当网站接收到第一个请求时,它将加载VisionDAL.dll,如果您现在重新构建项目,您将收到一个关于VisionDAL.dll的写入错误:要修复它,请重置应用程序池或使用unlocker之类的工具。

然后我们指定VisionDAL的包含属性。在"配置属性"/"C/C++"下,添加您的MySQL头文件路径,例如:C:\Program Files\MySQL\MySQL Server 5.6\include

接下来,我们在"C/C++"/预处理器/"预处理器定义"下,将"**预编译头**"切换为"**不使用预编译头**",以避免在使用strcpyfopen时出现错误消息,设置预处理器定义:USE_STANDARD_FILE_FUNCTIONS_CRT_SECURE_NO_WARNINGS

现在链接时,mysqllib引用无法解析,因为它们是64位过程。通过打开VisionDAL的项目属性,选择"**配置管理器**"按钮,并将平台设置为x64来修复此问题。

现在我们创建名为Vision的样本数据库

在SQL Development下打开MySql Workbench,打开您的实例。会出现一个新窗口"SQL File 1"。双击VisionDAL项目中的Sql.txt文件。将所有内容复制到剪贴板,粘贴到工作台中"SQL File 1"窗口中。点击闪电图标(左侧的第三个图标)创建样本数据库。

接下来我们需要数据库登录的常用信息

我们为此有一个配置文件:<installation director>VisionSmall\x64\Debug\VisionConfiguration.txt,它看起来是这样的

Host: localhost
User: root
Password: frob4frob
Database: vision
Port: 3306

修改值以匹配您的MySQL配置。

Vision数据库

数据库中只有一个表

    CREATE TABLE `document` (
  `DocumentID` int(11) NOT NULL AUTO_INCREMENT,
  `Title` varchar(255) DEFAULT NULL,
  `Text` text,
  PRIMARY KEY (`DocumentID`),
  FULLTEXT KEY `ft` (`Title`,`Text`),
  FULLTEXT KEY `ftTitle` (`Title`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

搜索时,我们使用名为`ft`的fulltext索引;在查找自动完成单词时,我们使用名为'ftTitle'的fulltext索引。

如果您有一个包含多个字段的fulltext索引,您可以在Microsoft SQL Server查询时选择应包含在搜索中的字段。在MySQL中,始终搜索fulltext索引中的所有字段,因此我们需要指定附加的fulltext索引'ftTitle'。

使用C接口查询MySQL

首先,我们需要连接到数据库并获取一个MYSQL指针以供进一步访问

        MYSQL *Connect(){
	MYSQL *conn; // Connection

	// Connect to MySQL 
	conn = mysql_init(NULL);
	if(mysql_real_connect(
		conn, Configuration.Host, Configuration.User, Configuration.Password,
		Configuration.Database, Configuration.Port, NULL, 0) == NULL) {
			fprintf(stderr, "sorry, no database connection ...\n");
			return NULL;
	}
	return conn;
}    

启动时,我们使用配置文件VisionConfiguration.txt中的值填充全局Configuration struct,该文件应与VisionDAL.dll在同一目录中。读取设置的例程是ConfigurationRead。为了获取当前正在执行模块的路径,它使用Win32 API的GetModuleFileName

TCHAR *GetExecutablePath(){
	HMODULE hMods[1024];
    	HANDLE hProcess;
    	DWORD cbNeeded;
    	unsigned int i, j;
	DWORD processID = 0L;
	TCHAR fileName[MAX_PATH];
	TCHAR *szModName=(TCHAR *) malloc(MAX_PATH*2);
	int size=MAX_PATH*2;

	processID = GetCurrentProcessId();
	// Get a handle to the process.
    	hProcess = OpenProcess( PROCESS_QUERY_INFORMATION |
                            PROCESS_VM_READ,
                            FALSE, processID );
	if (NULL == hProcess)
        return NULL;
	if( EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)){
        for ( i = 0; i < (cbNeeded / sizeof(HMODULE)); i++ ){
			szModName[0] = 0;
			fileName[0]=0;
            // Get the full path to the module's file.
            if ( GetModuleFileNameEx( hProcess, hMods[i], szModName,
                                     size )){
		int lastBackSlash=0;
		int k=0;
		for(k=0; k <(int) wcslen(szModName) -1; k++){
			if(szModName[k] == '\\') lastBackSlash = k;
			}
			if(lastBackSlash == 0) continue;
			szModName[lastBackSlash]=0;
			for(k=lastBackSlash + 1, j=0; szModName[k] != 0; k++, j++){
				fileName[j]=szModName[k];
			}
			fileName[j]=0;
			wStrLower(fileName);
			if(!wcscmp(L"visiondal.dll", fileName)){
					return szModName;
			}
            }
        }
	}
	return NULL;
} 

我们只想公开一个例程:GetDocuments。在头文件中的定义

#define FORMAT_TEXT 0
#define FORMAT_JSON 1>
#define FORMAT_DATATABLE 2 
__declspec(dllexport) wchar_t*   __cdecl GetDocuments(TCHAR *search, int format, int forAutocomplete);

在源文件中的定义

__declspec(dllexport) wchar_t* GetDocuments(TCHAR *search, int format, int forAutocomplete)

声明和定义上的__declspec(dllexport)效果是将调用添加到VisionDAL.lib文件并导出到VisionDAL.dll文件中。__cdecl定义了如何调用过程,这里我们使用C风格的调用约定。TCHAR是一个定义,当定义了UNICODE时,它与WCHAR相同,否则它是一个简单的char,在我们的例子中,UNICODE已打开。

  • 请注意,存在不同的Unicode格式
  • 在C代码中,我们使用2字节值来表示char
  • 在MySQL和.NET Framework中,格式是UTF-8,这意味着每个字符使用一个字节,并且只有在需要时才使用多个字节。
  • 在控制台应用程序中,您通常为每个char使用一个字节,并使用代码页850处理大于127的值。

参数格式为FORMAT_TEXTFORMAT_JSON,用于在文本和JSON之间切换输出。版本2添加了FORMAT_DATATABLE格式,该格式将输出格式化为DataTable的标准格式。

如果forAutocompletetrue,则只搜索并返回Title

VisionDALClientConsole

VisionDALClientConsole是一个微小的Windows控制台应用程序,用于测试我们的GetDocuments过程。

它设置了对VisionDAL项目的引用。它的输出文件与VisionDAL的输出文件一起转到VisionSmall\x64\Debug

VisionDALClientConsole要求输入搜索字符串,通配符是"*",它搜索title和text列,并输出GetDocuments调用返回的文本。

一个示例运行

Console Output

主例程

int _tmain(int argc,TCHAR* argv[])
{
	char c;
	TCHAR *result;
	TCHAR *search = (TCHAR *)malloc(1000*2);
	char *searchA = (char *)malloc(1000);
	int retval = 1;
	char buffer[32000];

	buffer[0]=0;
	printf("Search for: ");
	/* wscanf doesn't get umlauts */
	if(scanf("%[^\n]", searchA) <= 0){
		printf("Could not read input - retrieving all Documents \n");
		*search=0;
	}else{
		MultiByteToWideChar(850,0,searchA, -1,search, 999);
	}
	result=GetDocuments(search, FORMAT_TEXT, 0);
	if(result == NULL){
		retval = 0;	
	}else{
		WideCharToMultiByte(850,0,result, -1,buffer, 32000,NULL,NULL);
		printf("%s", buffer);
	}
	fflush(stdin);
	printf("Press RETURN Key to Exit\n");
	getchar();
	return retval;
}  

在Microsoft C V.12中,有一些例程可以处理Unicode-16字符串。它们有一个起始的附加w,或者将str替换为wcs,例如:wscanfwprintf而不是scanfprintf,以及wcslen而不是strlen。使用wscanf未能正确处理带音标的字母。我使用了MultiByteToWideChar(使用代码页850)来获取宽字符,并使用WideCharToMultiByte转换回char

查询MySQL数据库

上面,我展示了如何连接数据库并获取一个名为conn的MYSQL指针。

接下来,我们构建SQL查询,这是为自动完成生成列表的代码

mysql_query(conn, "SET NAMES 'utf8'");
if(forAutocomplete){
	if(search == NULL || wcslen(search) ==0){
		WideCharToMultiByte(CP_UTF8,0,L"SELECT Title from Document LIMIT 20",-1,
                sql,1000,NULL,NULL);
	}else{
		wsprintf(lbuffer, L"SELECT Title, match(Title) against('%ls' IN BOOLEAN MODE) 
                as Score from Document where match(Title) against('%ls' IN BOOLEAN MODE) > 0.001 
                order by Score Desc LIMIT 20",
			search, search);
		WideCharToMultiByte(CP_UTF8,0,lbuffer,-1,sql,1000,NULL,NULL);
	}
}

match(Title, Text) against('%ls' IN BOOLEAN MODE) TitleText列中搜索搜索字符串,并返回匹配程度的值。只显示Score大于0.001的文档,结果按分数排序。

IN BOOLEAN MODE表示对多个单词的搜索被解释为

搜索字符串中,您可以使用"*"作为通配符,它将匹配0到n个字符。例如,"as*"将找到ASP。搜索不区分大小写。有些情况很烦人:"as**"找不到任何东西;"*sp"或"*"不匹配任何内容 - 在Microsoft SQL Server中,您可以在字符串的开头匹配通配符。

获取数据

...
if(mysql_query(conn, sql)) {
    fprintf(stderr, "%s\n", mysql_error(conn));
    fprintf(stderr, "%s\n", sql);
    return NULL;
}
 ...
// Process results
result = mysql_store_result(conn);
...
    while((row = mysql_fetch_row(result)) != NULL) {
        ...
	    }else if(format == FORMAT_JSON){
			if(!forAutocomplete){
				MultiByteToWideChar(CP_UTF8,0,row[0], -1,buffer, 255);
				wsprintf(resultBufferp,L"{\"DocumentID\": %s, ", buffer);
				resultBufferp+=wcslen(buffer)+wcslen(L"{\"DocumentID\": , ");
				if(row[1]){
					MultiByteToWideChar(CP_UTF8,0,row[1], -1,buffer, 255);
					wsprintf(resultBufferp,L"\"Title\": \"%s\", ", buffer);
					resultBufferp+=wcslen(buffer)+wcslen(L"\"Title\": \"\", ");
				}else{
					wsprintf(resultBufferp,L"\"Title\": null, ");
					resultBufferp+=wcslen(L"\"Title\": null, ");
				}
				if(row[2]){
					MultiByteToWideChar(CP_UTF8,0,row[2], -1,buffer, 32000);
					wsprintf(resultBufferp,L"\"Text\": \"%s\"},", buffer);
					resultBufferp+=wcslen(buffer)+wcslen(L"\"Text\": \"\"},");
				}else{
					wsprintf(resultBufferp,L"\"Text\": null},");
					resultBufferp+=wcslen(L"\"Text\": null},");
				}
			}
	    ...
        }
    ...
	}

mysql_query将查询发送到服务器。mysql_store_result将结果准备为一个集合,您可以使用mysql_fetch_row(result)进行迭代。行是字符串的数组,忽略列的数据类型。我更喜欢ADO.NET中的类型化列。在.NET中,我们可能会使用StringBuilder来聚合结果字符串,在这里我们用malloc分配一个char[]并增加指针resultBufferp,然后我们写入。我使用MultiByteToWideChar转换为WCHAR

JSON格式

我决定使用轻量级的JSON格式,用于从网页通过Ajax与Webservice通信,而不是XML。

使用FORMAT_DATATABLE时的JSON输出如下所示

[{"DocumentID": 1, "Title": "ASP MVC 4", "Text": "Was für Profis"},
{"DocumentID": 2, "Title": "JQuery", "Text": 
"Hat Ajax Support"},{"DocumentID": 3, "Title": "
WebServices", "Text": "Visual C++ kanns nicht"},
{"DocumentID": 4, "Title": "Boost", "Text": "Muss Extra installiert werden"}]
    

当参数autocomplete为true时的JSON输出如下所示

["ASP MVC 4","JQuery","WebServices","Boost"]

DataTable期望一个二维数组,看起来像这样

        [[1, "ASP MVC 4", "Was für Profis"],[2, "JQuery", "Hat Ajax Support"]]

但也可以使用对象数组,如上所示,用于FORMAT_DATATABLE格式。

""[]""表示数组的开始和结束,""{}""表示对象的开始和结束。在对象中,":"之前的部分是属性名,之后的部分是值。这与您在JavaScript中编写值时几乎相同。使用JavaScript命令JSON.parse,您可以获得一个完整的对象,其属性可以通过常用的"."表示法访问。

托管GetDocuments方法的WebService

我使用"Visual C#/WCF/WCF服务应用程序"模板创建了VisionWeb项目,它添加了所需的引用,如System.ServiceModel

接下来,我们使用NuGet添加所需的JavaScript库。选择"工具/程序包管理器/程序包管理器控制台",然后输入命令

Install-Package jQuery
Install-Package jQuery.UI.Combined

接下来我们在"App-Code/IVisionService.cs"文件中定义服务契约

 namespace VisionServices
{
    [ServiceContract(SessionMode = SessionMode.Allowed)]
    public interface IVisionService
    {
        [OperationContract]
        [WebInvoke(
            Method = "POST",
            BodyStyle = WebMessageBodyStyle.WrappedRequest,
            RequestFormat = WebMessageFormat.Json,
            ResponseFormat = WebMessageFormat.Json)]
        string GetDocuments(string search, int format, int forautocomplete);    
    }   
}

WebInvoke属性确保服务可以通过Ajax调用。作为方法,我选择了POST,它将参数发送到HTTP请求的正文中。备选的GET会将参数编码并显示在URL中。

我们指定请求和响应都以JSON格式发送。当使用多个参数时,必须使用BodyStyle = WebMessageBodyStyle.WrappedRequest。如果您有零个或一个参数,可以使用WebMessageBodyStyle.Bare

WebService的实现

我们在"App-Code/VisionService.cs"中定义GetDocuments的实现

namespace VisionServices
{
    public class PInvoke
    {
        [DllImport("VisionDAL.dll", CharSet = CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.LPWStr)]
        public static extern string GetDocuments(string search, int format, int forAutocomplete);
    }
    public class VisionService : IVisionService
    {
        public string GetDocuments(string search, int format, int forautocomplete)
        {
            string result = PInvoke.GetDocuments(search, format, forautocomplete).ToString();
            return result;
        }
    }
}

VisionService.svc的实现

<%@ ServiceHost Language="C#" Debug="true" Service="VisionServices.VisionService" 
    CodeBehind="App_Code\VisionService.cs" %>

这定义了我们的服务可以调用的端点为"http://<您的Web服务器>:<您的端口>VisionService.svc",调用我们GetDocuments函数的URL为"http://<您的Web服务器>:<您的端口>/VisionService.svc/GetDocuments"

Web.config

<?xml version="1.0"?>
<configuration>
  <appSettings/>
  <system.web>
    <httpRuntime/>
    <compilation debug="true"/>
  </system.web>
  <system.serviceModel>
    <services>
      <service name="VisionServices.VisionService">
        <endpoint address="" binding="webHttpBinding" 
         contract="VisionServices.IVisionService" behaviorConfiguration="webHttpEndpoint"/>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>
    </services>
    <behaviors>
      <endpointBehaviors>
        <behavior name="webHttpEndpoint">
          <webHttp helpEnabled="true"/>
        </behavior>
      </endpointBehaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="false" 
     multipleSiteBindingsEnabled="true"/>
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
    <directoryBrowse enabled="true"/>
  </system.webServer>
</configuration>

这些设置允许Ajax调用。WCF有很多配置选项。您可以查阅更进一步的文档,例如[2],它可以在Safari上找到。

<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>定义了元数据交换的端点。从元数据中,您可以自动生成代码来获取WebService的代理,例如使用svcutil。选择"程序/Microsoft Visual Studio 2012/Visual Studio Tools/Developer Command Prompt for VS2012"。输入svcutil https://:8001/VisionService.svc。会生成一个VisionService.cs文件,在其他情况下,也会生成一个包含WebService配置的文件。

请注意,Visual Studio有一个用于WebService配置的图形编辑器。它位于"工具/WCF服务配置编辑器"下。

托管网站

启动"设置/控制面板/管理工具/Internet Information Services (IIS)管理器"。如果缺少,请安装IIS。导航到"**应用程序池**",选择一个运行.NET Framework 4.0*的池的名称,或添加一个新的应用程序池。导航到"**网站**"节点,右键单击它并选择"**添加网站...**"。使用Vision作为网站名称,对于应用程序池,选择一个运行.NET Framework 4的池,物理路径使用<vision安装目录>/VisionWeb,端口设置为8001。在VisionWeb项目上选择属性,选择Web,选中"**使用自定义Web服务器**",在服务器URL中输入https://:8001。您可以选择其他选项托管网站,例如在IIS Express中,但如果您不想修改Default.htmlDefaultQueryOnly.html文件,则必须将端口设置为8001

HTML/JQuery页面

VisionWeb版本1项目中,有一个名为"DefaultQueryOnly.html"的单个HTML页面,其中包含HTML和JavaScript内容。在版本2中,解决方案以"Default.html"作为起始页。

再次看看网页的样子

Search Page

 <html>
<head>
    <title>Search</title>
    <script src="Scripts/jquery-2.0.2.js"></script>
    <script src="Scripts/jquery-ui-1.10.3.js"></script>
    <link href="Content/themes/base/jquery.ui.autocomplete.css" rel="stylesheet" />
    <style type=text/css>
        .ui-menu-item {
            background: White;
        }
       .ui-helper-hidden-accessible { display:none; }
    </style>
</head>

<code>html</code>声明它具有HTML 5的文档类型。然后,我们包含必要的JavaScript文件。从JQuery-UI,我们只使用autocomplete小部件,为此我们包含了它的CSS文件。

对于包含.ui-menu-item类的autocomplete对象,我们将背景设置为白色,否则它是透明的,并且表格的内容会显示出来。.ui-helper-hidden-accessible { display:none; } 删除了autocomplete小部件中一个烦人的帮助消息。

<form>
    <label for="search" >Search:</label>
    <input type="text" id="search", name="search"  />
    <input type="button" id="update" name="update" value="Update"  />
    <div id="result"></div>
</form>

表单中的元素被赋予了ID,以便您可以使用JQuery检索它们,例如$('#result')。而不是快捷方式$,您可以使用jQuery,例如[__em all="[object HTMLCollection]"__] jQuery('#result')。JavaScript调用document.getElementById('result')具有相同的效果,但JQuery支持各种CSS选择器以及更多内容。

我使用非侵入性JavaScript,这意味着HTML代码不会与JavaScript混合。eventhandler$(document).ready(function ()函数中设置,该函数在页面加载并准备就绪后被调用。

function GetDocuments(e) {
    var searchstring = AddWildcards($('#search').val());
    $.ajax({
        type: 'POST',
        url: 'https://:8001/VisionService.svc/GetDocuments',
        dataType: 'json',
        crossDomain: true,
        data: JSON.stringify({ search: searchstring, format: 1, forautocomplete: 0 }),
        processData: true,
        contentType: "application/json ; charset=utf-8",
        success: function (json, textStatus) {
            var result = JSON.parse(json);
            var display;
            display = "";
            display += "<table id='mytable' border=2>  
                        <thead><tr><th style='text-align:left' >ID</th>
                        <th style='text-align:left' >Title</th><th style='text-align:left' >Text</th>
                        </tr></thead>";
            display += "<tbody>";
            $.each(result, function (index, value) {
                display += "<tr>";
                display += "<td>" + value.DocumentID + "</td>";
                display += "<td>" + ((value.Title != null)?value.Title:"") + "</td>";
                display += "<td>" + ((value.Text != null)?value.Text:"") + "</td>";
                display += "</tr>";
            });
            display += "</tbody></table>";
            $('#result').empty()
            $('#result').html(display);
        },
        error: function (xhr, textStatus, errorThrown) {
            alert('An error occurred! ' + (errorThrown ? errorThrown : xhr.status) + 
                  " xhr: " + xhr + " textStatus: " + textStatus);
        }
    });
}

当您单击"更新"按钮或在搜索字段中按Enter键时,将执行GetDocuments调用。它执行全文查询并在HTML表中显示结果。

我们在searchstring变量中获取搜索表单字段的值,并在AddWildcards中为它的每个单词添加通配符*,如果它不存在的话。JQuery具有AJAX支持,例如$.ajax调用。

您可以在这里找到此调用的参考:jQuery.ajax()

url:指向我们在WCF应用程序中配置的地址。正如我们在WCF应用程序中设置的那样,我们使用JSON作为数据格式。在success:函数中(它在ajax调用成功时异步调用),我们在json变量中获得我们GetDocuments调用的输出。一个简单的调用JSON.parse(json),会得到一个完整的JavaScript对象,我们从中生成一个HTML表。变量result是一个JavaScript对象数组。jQuery的$.each调用迭代数组,并调用给定的函数,参数为当前数组元素的索引和当前位置的对象。使用$('#result').html(display),我们将HTML字符串display显示为我们结果div的HTML。在data:下,我们将调用参数指定为JavaScript对象,并使用JSON.stringify调用为传输做准备。当发生错误时,error: 后面的代码将被执行。

自动完成的工作原理

在我们的JavaScript代码的开头,我们声明一个全局变量来存储autocomplete的单词在一个数组中: var Documents = [];GetAutocomplete函数填充Documents数组。Autocomplete函数

 function GetAutocomplete(e) {
   var searchstring = $('#search').val();
    if (searchstring.length > 0) {
        if (searchstring[searchstring.length - 1] != "*") {
            searchstring += "*";
        }
    }
    $.ajax({
        type: 'POST',
        url: 'https://:8001/VisionService.svc/GetDocuments',
        dataType: 'json',
        data:  JSON.stringify({ search: searchstring, format: 1, forautocomplete: 1}),
        processData: true,
        async: false,
        contentType: "application/json ; charset=utf-8",
        success: function (json, textStatus) {
            Documents = JSON.parse(json);
        },
        error: function (xhr, textStatus, errorThrown) {
            alert('An error occurred! ' + (errorThrown ? errorThrown : xhr.status) + 
                  " xhr: " + xhr + " textStatus: " + textStatus);
        }
    });
}

这看起来与GetDocuments函数非常相似。success函数仅使用JSON.parse解析我们Webservice的输出来更新Documents变量。注意async: false,它会导致调用同步进行。autocomplete小部件将调用我们的GetAutocomplete函数并立即显示Documents

$(document).ready(function ()中从autocomplete小部件初始化

$("#search").autocomplete({
    source: function (request, callback) {
        GetAutocomplete();
        callback(Documents);
    },
    open: function (event) {
        var $ul = $(this).autocomplete("widget");
    }
});

您可以在这里找到关于autocomplete的信息。

处理搜索框中的**[RETURN]**键

 $('#search').bind("keydown", GetInput);
function GetInput(e) {
            if (e.keyCode == 13) {
                e.preventDefault();
                GetDocuments(e);
                $('#search').autocomplete("close");
            }
        }

e.preventDefault();停止当前事件的处理。

调试

您可以在Web浏览器中输入URLhttps://:8001/VisionService.svc。如果发生服务激活错误,将显示一条消息,例如无法加载VisionDAL.dll,或未安装WCF,这将导致一条关于缺少处理程序的错误消息。您可以使用Fiddler等工具检查HTTP通信。

调试VisionDAL时,运行VisionWeb不起作用。无调试地启动Vision并附加到w3wp.exe

VisionSmall 版本2 - 实现CRUD操作

到目前为止,我向您展示了DefaultQuery,现在我们切换到Default.html

我们点击"工具/程序包管理器/管理解决方案的NuGet程序包...",搜索jquery.datatables,然后添加结果包。

我们添加一个名为oTable的全局变量来保存对创建的表的引用。我们将GetDocuments修改为使用DataTable

function GetDocuments(e) {
    var searchstring = AddWildcards($('#search').val());
    $.ajax({
        type: 'POST',
        url: 'https://:8001/VisionService.svc/GetDocuments',
        dataType: 'json',
        data: JSON.stringify({ search: searchstring, format: 2, forautocomplete: 0 }),
        contentType: "application/json ; charset=utf-8",
        success: function (json, textStatus) {
            var result = JSON.parse(json);
            var display;
            display = "";
            display += "<table id='mytable' border=2>  
                        <thead><tr><th style='text-align:left' >ID</th>
                        <th style='text-align:left' >Title</th>
                        <th style='text-align:left' >Text</th></tr></thead>";
            display += "<tbody>";
            display += "</tbody>";
            display += "</table>";
            if (oTable) {
                $('#mytable').remove();
                oTable = null;
            }
            $('#result').empty()
            $('#result').html(display);                   
            oTable = $('#mytable').dataTable({
                "aaData": result,
                "bPaginate": false,
                "bLengthChange": false,
                "bFilter": false,
                "bSort": true,
                "bInfo": false,
                "bAutoWidth": false
            });
            ...
    }
    ...
}

oTable = $('#mytable').dataTable(将HTML表转换为DataTable。表的内容通过 "aaData": result,属性传递。表头与版本1一样,通过发出HTML代码生成。相反,您可以为DataTable指定列。

function makeSelectable(e) {
    if ($(this).hasClass('row_selected')) {
        $(this).removeClass('row_selected');
    }
    else {                
        $(this).addClass('row_selected');
    }
};

实现删除

首先,通过在我们的GetDocuments函数中调用oTable.$("tbody tr").click(makeSelectable)来使tablerows可选择。之后,只有第一列是可选择的,因为我们将editable应用于所有列,除了第一列"ID"。

/* Get the rows which are currently selected */
function fnGetSelected(oTableLocal) {
    var aReturn = new Array();
    var aTrs = oTableLocal.fnGetNodes();

    for (var i = 0 ; i < aTrs.length ; i++) {
        if ($(aTrs[i]).hasClass('row_selected')) {
            aReturn.push(aTrs[i]);
        }
    }
    return aReturn;
}
...
    $('#delete').click(function () {
        if(!oTable)return;
        var ids = new Array();
        var anSelected = fnGetSelected(oTable);
        if (anSelected.length == 0) return;
        for (i = anSelected.length - 1; i >= 0; i--) {
            // anSelected[i].cells[0].innerText works with IE and Chrome 
            // but not in Firefox(version 23.01)
            if (anSelected[i].cells[0].innerHTML != "" && anSelected[i].cells[0].innerHTML != null) {
                ids.push(parseInt(anSelected[i].cells[0].innerHTML));
                oTable.fnDeleteRow(anSelected[i], null, false);
            }
        }
        oTable.fnDraw();
        $.ajax({
            type: 'POST',
            url: 'https://:8001/VisionService.svc/DeleteDocuments',
            dataType: 'json',
            data: JSON.stringify({"ids": ids}),
            contentType: "application/json ; charset=utf-8",
            asyn: false,
            success: function (json, textStatus) {
                var i = JSON.parse(json);
                if(i != ids.length) alert("Error: should delete " + ids.length  +  
                   " but deleted " + i + " records");
            },
            error: function (xhr, textStatus, errorThrown) {
                alert('An error occurred! ' + (errorThrown ? errorThrown : xhr.status) + 
                      " xhr: " + xhr + " textStatus: " + textStatus);
            }
        });
    });
   ...

我们迭代oTable中选定的表行,并调用DataTable函数fnDeleteRow,该函数从表中删除给定的行。

Ajax调用由WCF项目中的DeleteDcouments处理

public int DeleteDocuments(int[] ids)
{
    int retval = -1;

    IntPtr intPtr = Marshal.AllocHGlobal(ids.Length * sizeof(int));
    Marshal.Copy(ids, 0, intPtr, ids.Length);
    retval = PInvoke.DeleteDocuments(intPtr, ids.Length);
    Marshal.FreeHGlobal(intPtr);
    return retval;
}

请注意,WCF足够智能,可以自动将JSON数组转换为C# int数组。但为了调用我们的C例程,我们必须使用Marshal函数准备一个原始C数组。

DAL中的C例程

...
if(mysql_query(conn, buffer)) {
...
printf("%i records found\n", retval=mysql_affected_rows(conn));

该例程首先生成一个SQL字符串,例如

 Delete from Document where DocumentID in (2,4)

. mysql_query将SQL字符串发送到服务器。请注意,无论调用是读取、更新、删除还是新语句,都使用mysql_querymysql_affected_rows返回最后mysql_query语句影响的行数,在本例中为已删除的行数。

实现新增

    ...
$("#new").unbind('click');
$('#new').click(function () 
{
    if(!oTable)return;
    var id=-1;    // generated on the server
    var Title=null;
    var Text=null;
    $.ajax({
        type: 'POST',
        url: 'https://:8001/VisionService.svc/NewDocument',
        dataType: 'json',
        data: JSON.stringify({"title": Title, "text": Text }),
        contentType: "application/json ; charset=utf-8",
        aysnc: false,
        success: function (json, textStatus) {
            id = JSON.parse(json);
            if (id == -1) {
                alert("Error: inserting a new record failed");
                return -1;
            }
            var values = new Array(id, Title, Text);
            oTable.fnAddData(values);
            // var tds = oTable.$("tr:last-child td:gt(0)"); // doesn't work
            $("#mytable tr:last-child td:gt(0)").addClass("editable").editable(makeEditable, 
                { "placeholder": "" });
            oTable.$("tbody tr:last-child").click(makeSelectable);
            $("#mytable tr:last-child td:eq(1)").click();
        },
        error: function (xhr, textStatus, errorThrown) {
            alert('An error occurred! ' + (errorThrown ? errorThrown : xhr.status) + 
                  " xhr: " + xhr + " textStatus: " + textStatus);
        }
    });
});
...

目前TitleText未被使用。我们在数据库中生成一个空记录,这会导致生成一个新的DocumentID,将其设置在新记录中并返回给调用的ajax函数。提交的SQL如下所示

Insert Document(Title) values(NULL)

我们使用DataTable调用oTable.fnAddData(values)将新记录添加到表中。然后我们需要使行可选择和可编辑。

实现更新

我们从editable下载jQuery editable。

GetDocuments函数中,我们绑定了来自New按钮的click事件。

 ...
oTable.$('td:gt(0)').addClass("editable").editable
(makeEditable,{ "placeholder": "" }); // when placeholder is not set empty cells display 
                                      // "Click to edit"
...
function makeEditable(value, settings) {
    var columnName = this.parentNode.parentNode.parentNode.rows[0].cells[this.cellIndex].innerHTML;
    var id = parseInt(this.parentNode.cells[0].innerHTML);
    $.ajax({
        type: 'POST',
        url: 'https://:8001/VisionService.svc/UpdateDocument',
        dataType: 'json',
        data: JSON.stringify({ "id": id, "columnName": columnName, "newValue": value }),
        async: false,
        contentType: "application/json ; charset=utf-8",
        success: function (json, textStatus) {
            var i = JSON.parse(json);
            if (i != 1) alert("Error: should update " + ids.length + " but updated " + i + " records");
        },
        error: function (xhr, textStatus, errorThrown) {
            alert('An error occurred! ' + (errorThrown ? errorThrown : xhr.status) + 
                                " xhr: " + xhr + " textStatus: " + textStatus);
        }
    });
    return (value);
};

当一个HTML元素被设置为可编辑时,它会编辑对象,当您单击它一次。新数据将被提交。因此,每次更新都会修改单个列。

生成的SQL如下所示

Update Document set Title='new value' where DocumentID=4

参考文献

  • [1]Michael Kofler;MySQL 5: Einführung, Programmierung, Referenz;Addison-Wesley Verlag 2009
  • [2]Steven Cheng; Microsoft Windows Communication Foundation 4.0 Cookbook for Developing SOA Applications;Packt Publishing 2010
  • [3]DataTables
  • [4]editable
© . All rights reserved.