1.使用程序设计语言访问SQL
SQL查询不是万能的,我们还需要使用通用程序设计语言,至少有两点原因。
SQL不能表达所有的查询,对于复杂查询,我们可以把SQL嵌入到一种更加强大的语言做到。
非声明式动作不能够在SQL中完成(比如打印一份报告,和用户交互)。
可以通过两种方式从通用语言中访问SQL。
动态SQL(dynamic SQL)。通用程序可以通过一组函数或者方法连接数据库并与之通信,动态SQL允许在程序运行时以字符串形式构建SQL查询,提交查询,然后每次以一个元组的方式把结果存入程序变量中。这一篇文章我们将介绍用于java的应用程序接口JDBC,以及ODBC(最初为C开发,后来应用于C,C++,C#,Ruby,Go,PHP和Visual Basic等)。并介绍Python Database Api怎么连接到数据库。对于为VB和C#语言设计的ADO.NET API,本文不做介绍,可以参考相关手册。
嵌入式SQL(embedded SQL)。SQL语句在编译时采用预处理器来进行识别,预处理器用嵌入式SQL表达的请求转换为函数调用。在运行时,这些函数调用将使用动态SQL设施的API连接到数据库,但这些API可能只适用于正在使用的数据库。
把SQL与通用语言相结合的主要挑战是SQL与这些语言操作数据的方式不匹配,在SQL中,数据的主要类型是关系,SQL操作关系,返回结果也是关系,在程序设计语言中,数据操作的基本单元是变量。需要提供一种机制做这样的转换。
1.1 JDBC
JDBC提供了java程序连接到数据库服务器的应用程序接口。
下面示例是Java使用JDBC的一个示例,Java程序必须加在java.sql.*,它包含了JDBC所提供函数的接口定义。
public static void JDBCexample(String userid,String passwd){ try( // 获取连接 // 参数1,通信协议:主机名称:端口号:使用的特定数据库 // 参数2,数据库用户标识 // 参数3,密码 Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@db.yale:edu:1521:univdb",userid, passwd); // 创建一个Statement(在获取连接后执行SQL语句的对象) Statement stmt = conn.createStatement(); ) { try{ //以字符串形式构建SQL语句 stmt.executeUpdate("insert into instructor values('77987','Kim','Physics',98000)"); } catch(SQLException sqle) { System.out.println("Could not insert tuple:" + sqle); } // 查询获取结构,并将结果以"元组"方式存储到变量中 ResultSet rset = stmt.executeQuery("select dept_name,avg(salary)" + " from instructor" + " group by dept_name"); while(rset.next()) { System.out.println(rset.getString("dept_name") + " " + rset.getFloat(2)); } } catch(Exception sqle) { System.out.println("Exception:" + sqle); } }
1.1.1 连接到数据库
getConnection()有三个参数。
参数1,通信协议:主机名称:端口号:使用的特定数据库。JDBC驱动会支持很多种协议,我们需要选择一个数据库和驱动器都支持的协议,协议的详细内容是由产商设定的;
参数2,数据库用户标识;
参数3,密码。注意,在JDBC代码中直接指定密码会带来安全性风险,这里仅仅是为了简便这么写。
所有的主流产商都支持JDBC,这些数据库产品都会提供一个JDBC驱动程序,该驱动程序必须在连接到数据库前被动态的加载才能数显Java对数据库的访问。如果已经从产商的网站下载了合适的驱动程序,getConnection()方法将定位所需要的驱动程序,从而实现面向产品的调用。
用于与数据库交换数据的协议实际上取决于JDBC驱动,协议是根据数据库产品的支持进行选择的,示例中使用的是:jdbc:oracle:thin,Mysql支持的协议是:jdbc:mysql
1.1.2 向数据库系统中传递SQL语句
我们通过连接句柄conn创建了Statement 对象,该对象用来向数据库系统中传递SQL语句。
1.1.3 异常与资源管理
try...catch结构用来处理异常。
打开连接、语句和其他JDBC对象都会消耗系统资源,必须及时关闭,否则数据库资源池会耗尽。关闭资源的一种方式是显示调用关闭,这种方式存在隐患,如果代码异常退出,此方法将会失效,你可以通过将其放在finally结构中解决或者使用示例中推荐的第二种方式。第二种方式是try-with-resources结构,它将连接和语句对象放在try中完成,简洁明了,自动隐式关闭该语句的对象,是首选方式。
1.1.4 获取查询结果
示例通过stmt.executeQuery获取查询结果,存储到ResultSet 变量中,通过getString可以获取所有类型的结果对象并且将其转化为String,也可以通过getFloat这种约束性更强的方式提取结果中的属性。提取属性可以通过名称(getString(dept_name))或者位置(getFloat(2))进行.
1.1.5 预备语句
我们可以创建一条预备语句,其中用"?"来代替某些值,以此指明以后会对其提供实际的值。数据库系统在预备查询的时候对其进行编译,在执行查询时(用新值代替“?”),数据库系统可以重用此前编译的查询形式,将新的值作为参数来应用。下面是一个示例。
在同一查询编译一次然后带不同的参数值运行多次的情况,预备语句使得执行更加高效。而且预备查询有个更大的有事,他可以避免用户手动拼接sql引入特殊字符(如多余的单引号,空格),从而生成具有语法错误的SQL。setString会自动检查用户输入,并且插入必须的转义确保语法的正确性,因此,预备语句是执行SQL的首选方法。
除此之外,使用预备语句还可以避免SQL注入来破坏或者窃取数据。
假如一个Java程序SQL如下。
"select * from instructor where name = '" + name + "'"
如果用户输入的参数name不是姓名,而是:
X' or 'Y' = 'Y
那么执行的SQL会变成:
select * from instructor where name = 'X' or 'Y' = 'Y'
本来用户只可以按姓名查找数据,现在他窃取了整个关系的数据!!!还有很多诡计多端的注入手段,窃取篡改数据。
使用预备语句可以避免这样的问题,因为查询的格式已经被预编译,用户输入的数据都被视为普通的字符串,会被插入转义字符,所以最后的查询会变成:
select * from instructor where name = 'X\' or \'Y\' = \'Y'
这是无害的查询,只会返回空的关系。
有些数据库系统允许在单个JDBC的execute方法执行多条SQL语句,语句之间用分号分隔。该特性在某些JDBC驱动中默认关闭了,因为它也可能带来SQL注入的风险。
对前面的SQL注入示例中,用户可以输入:
X';drop table instructor;--
这将导致很严重的问题。因此程序员必须使用预备语句进行查询。
1.1.6 可调用语句
JDBC还提供了CallableStatement接口,它允许调用SQL的存储过程和函数,它也用"?"来代替某些值,以此指明以后会对其提供实际的值,其返回值用registerOutParameter()方法注册,通过与结构集类似的get方法检索,可以参阅手册获取详情。
1.1.7 元数据特性
我们发现java应用程序中并不包含所存储数据的声明,这些声明是SQL DDL的一部分,因此只看java程序可能并不知道存储在数据库中的具体数据模式(当然你可以查看数据库,文档等),ResultMap接口提供了一个getMetaData()方法解决你的困难。
ResultSetMataData rsmd = rs.getMetaData(); // 获取属性个数并遍历 for(int i = 1; i <= rsdm.getColumnCount(); i++) { // 获取属性名称 System.out.println(rsdm.getColumnName(i)); // 获取属性类型 System.out.println(rsdm.getColumnTypeName(i)); }
除了关系的属性信息,还有很多其他的元数据:产品名称,版本号,数据库系统所支持的特性等。Connection接口可以获得一个DatabaseMetaData对象,DatabaseMetaData接口提供了查找关于数据库的元数据的方法。下图使用DatabaseMetaData查找列信息,其中getColumns第一个参数为null,表示其目录名称将被忽略,最后一个参数使用通配符%,表示匹配所有的列(名称)。
还有其它API请自行查阅手册。
元数据接口可以用于各种任务,例如,他们可以用于编写数据库浏览器,该浏览器允许用户查找数据库中的表,检查他们的模式,检查表中的行,应用选择来查看所需要的行等
1.1.8 其他特性
JDBC还有很多其他的特性。
可更新的结果集。根据在数据库关系上执行选择或者投影来创建出可更新的结果集,对结果集的更新将导致对数据库关系对应元组的更新。
事务的自动提交开启/关闭,事务回滚。通过Connection接口的setAutoCommit()方法与rollback()方法实现。
大对象处理接口。Result提供getBlob()和getClob()方法。PreparedStatement类提供setBlob,setClob方法
行集特性,允许收集结果集将其发送给其它应用程序,行集可以向前,向后扫描,并且可以被修改。
1.2 从Python访问数据库
可以通过如下方式完成。
注意,上面示例中查询语句不会自动提交到数据库,需要调用commit()方法。
程序第一行导入的是psycopg2驱动程序,这是连接到PostgreSQL的驱动程序。其他的产商驱动与python访问数据库的语法细节可以查阅手册。
1.3 ODBC
开放数据库连接(Open DataBase Connectivity, ODBC)标准定义了一个API,应用程序可以用它来与一个数据库的连接、发送查询和更新并获取返回结果。诸如图形化用户界面、统计程序包及电子表格那样的应用程序可以使用相同的ODBC API来连接到支持ODBC的任何数据库服务器。
ODBC的语法这里不做展开,SQL标准定义了一个与ODBC接口类似的调用层接口(Call level Interface,CLI)
1.4 嵌入式SQL
SQL标准允许将SQL嵌入到其他高级程序语言,嵌入了SQL查询的语言被称为宿主语言,在宿主语言中允许使用的SQL结构构成了嵌入式SQL。
嵌入式SQL程序在编译之前必须由特殊的预处理器进行处理,该预处理器将嵌入的SQL请求替换为宿主语言的声明以及允许运行时执行数据库访问的过程调用。然后,所产生的程序由宿主语言编译器进行编译。这就是嵌入式SQL与JDBC或者ODBC的主要区别。
为了使预处理器识别出嵌入式SQL请求,我们使用EXEC SQL语句,其格式如下。
EXEC SQL <嵌入式SQL语句>;
在执行任何SQL语句之前,程序必须受限连接到数据库,在嵌入式SQL语句中可以使用宿主语言的变量,不过他们的前面必须加上冒号:以将它们与SQL变量分开来。
要遍历一个嵌入式SQL查询的结果,我们必须声明一个游标变量,它可以随后被打开,并在宿主语言循环中发出获取(fetch)命令来获取查询结果的连续行。行的属性可以提取到宿主语言变量中,数据库更新也可以通过以下方式实现:使用关系上得游标来遍历关系的行,或者使用where子句来仅遍历所选的行。嵌入式SQL命令可用于更新游标所指向的当前的行。
嵌入式SQL请求的确切语法取决于嵌入SQL的语言,请参考手册。
JDBC中,SQL语句在运行时才进行解释,但在使用嵌入式SQL时,在预处理时就有可能捕获一些与SQL程序相关的错误(包括数据类型错误)。与在程序中使用动态SQL相比,嵌入式SQL程序中的SQL查询更容易理解。但是,嵌入式SQL也存在一些缺点,预处理器会创建新的宿主语言代码,这使得程序的调试变得更加复杂。并且当宿主语言的语法迭代时,还有可能发生语法冲突。
微软语言集成查询(LINO)使用嵌入式SQL,它扩展了宿主语言以包括对查询的支持,而不是使用预处理器将嵌入式SQL查询转换为宿主语言。除此意外,动态SQL仍然是主流。