Java 学习之路 之 执行SQL语句的方式(五十三)

本文详细介绍了JDBC执行DDL、DML等SQL语句的方法,包括使用executeUpdate()执行DDL和DML语句,使用execute()方法执行任意SQL语句,以及使用PreparedStatement提高执行效率并防止SQL注入。此外,文章还展示了如何使用CallableStatement调用存储过程,提供了完整的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面介绍了 JDBC 执行查询等示例程序,实际上,JDBC 不仅可以执行查询,也可以执行 DDL、DML 等 SQL 语句,从而允许通过 JDBC 最大限度地控制数据库。

1,使用 executeUpdate 方法执行 DDL 和 DML 语句

Statement 提供了 3 个方法来执行 SQL 语句,前面已经介绍了使用 executeQuery() 来执行查询语句,下面将介绍使用 executeUpdate() 来执行 DDL 和 DML 语句。使用 Statement 执行 DDL 和 DML 语句的步骤与执行普通查询语句的步骤基本相似,区别在于执行了 DDL 语句后返回值为 0,执行了 DML 语句后返回值为受影响的记录条数。

下面程序示范了使用 executeUpdate() 方法创建数据表。我们并没有直接把数据库连接信息写在程序里,而是使用一个 mysql.ini 文件(就是一个 properties 文件)来保存数据库连接信息,这是比较成熟的做法——当需要把应用程序从开发环境移植到生产环境时,无须修改源代码,只需要修改 mysql.ini 配置文件即可。

public class ExecuteDDL
{
	private String driver;
	private String url;
	private String user;
	private String pass;
	public void initParam(String paramFile)
		throws Exception
	{
		// 使用Properties类来加载属性文件
		Properties props = new Properties();
		props.load(new FileInputStream(paramFile));
		driver = props.getProperty("driver");
		url = props.getProperty("url");
		user = props.getProperty("user");
		pass = props.getProperty("pass");
	}
	public void createTable(String sql)throws Exception
	{
		// 加载驱动
		Class.forName(driver);
		try(
		// 获取数据库连接
		Connection conn = DriverManager.getConnection(url , user , pass);
		// 使用Connection来创建一个Statment对象
		Statement stmt = conn.createStatement())
		{
			// 执行DDL,创建数据表
			stmt.executeUpdate(sql);
		}
	}
	public static void main(String[] args) throws Exception
	{
		ExecuteDDL ed = new ExecuteDDL();
		ed.initParam("mysql.ini");
		ed.createTable("create table jdbc_test "
			+ "( jdbc_id int auto_increment primary key, " 
			+ "jdbc_name varchar(255), "
			+ "jdbc_desc text);");
		System.out.println("-----建表成功-----");
	}
}

mysql.ini 文件内容如下:

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/select_test
user=root
pass=32147

运行上面程序,执行成功后会看到 select_test 数据库中添加了一个 jdbc_test 数据表,这表明 JDBC 执行 DDL 语句成功。

使用 executeUpdate() 执行 DML 语句与执行 DDL 语句基本相似,区别是 executeUpdate() 执行 DDL 语句后返回 0,而执行 DML 语句后返回受影响的记录条数。下面程序将会执行一条 insert 语句,这条 insert 语句会向刚刚建立的 jdbc_test 数据表中插入几条记录。因为使用了带子查询的 insert 语句,所以可以一次插入多条语句。

public class ExecuteDML
{
	private String driver;
	private String url;
	private String user;
	private String pass;

	public void initParam(String paramFile)
		throws Exception
	{
		// 使用Properties类来加载属性文件
		Properties props = new Properties();
		props.load(new FileInputStream(paramFile));
		driver = props.getProperty("driver");
		url = props.getProperty("url");
		user = props.getProperty("user");
		pass = props.getProperty("pass");
	}
	public int insertData(String sql)throws Exception
	{
		// 加载驱动
		Class.forName(driver);
		try(
			// 获取数据库连接
			Connection conn = DriverManager.getConnection(url
				, user , pass);
			// 使用Connection来创建一个Statment对象
			Statement stmt = conn.createStatement())
		{
			// 执行DML,返回受影响的记录条数
			return stmt.executeUpdate(sql);
		}
	}
	public static void main(String[] args)throws Exception
	{
		ExecuteDML ed = new ExecuteDML();
		ed.initParam("mysql.ini");
		int result = ed.insertData("insert into jdbc_test(jdbc_name,jdbc_desc)" 
			+ "select s.student_name , t.teacher_name "
			+ "from student_table s , teacher_table t "
			+ "where s.java_teacher = t.teacher_id;");
		System.out.println("--系统中共有" + result + "条记录受影响--");
	}
}

运行上面程序,执行成功将会看到 jdbc_test 数据表中多了几条记录,而且在程序控制台会看到输出有几条记录受影响的信息。

2,使用 execute 方法执行 SQL 语句

Statement 的 execute() 方法几乎可以执行任何 SQL 语句,但它执行 SQL 语句时比较麻烦,通常我们没有必要使用 execute() 方法来执行 SQL 语句,而使用 executeQuery() 或 executeUpdate() 方法更简单。如果不清楚 SQL 语句的类型,则只能使用 execute() 方法来执行该 SQL 语句了。

使用 execute() 方法执行 SQL 语句的返回值只是 boolean 值,它表明执行该 SQL 语句是否返回 ResultSet 对象。那么如何来获取执行 SQL 语句后得到的 ResultSet 对象呢?

Statement 提供了如下两个法来获取执行结果。

getResultSet():获取该 Statement 执行查询语句所返回的 ResultSet 对象。

getUpdateCount():获取该 Statement() 执行 DML 语句所影响的记录行数。

下面程序示范了使用 Statement 的 execute() 方法来执行任意的 SQL 语句,执行不同的 SQL 语句时产生不同的输出。

public class ExecuteSQL
{
	private String driver;
	private String url;
	private String user;
	private String pass;
	public void initParam(String paramFile)throws Exception
	{
		// 使用Properties类来加载属性文件
		Properties props = new Properties();
		props.load(new FileInputStream(paramFile));
		driver = props.getProperty("driver");
		url = props.getProperty("url");
		user = props.getProperty("user");
		pass = props.getProperty("pass");
	}
	public void executeSql(String sql)throws Exception
	{
		// 加载驱动
		Class.forName(driver);
		try(
			// 获取数据库连接
			Connection conn = DriverManager.getConnection(url
				, user , pass);
			// 使用Connection来创建一个Statement对象
			Statement stmt = conn.createStatement())
		{
			// 执行SQL,返回boolean值表示是否包含ResultSet
			boolean hasResultSet = stmt.execute(sql);
			// 如果执行后有ResultSet结果集
			if (hasResultSet)
			{
				try(
					// 获取结果集
					ResultSet rs = stmt.getResultSet())
				{
					// ResultSetMetaData是用于分析结果集的元数据接口
					ResultSetMetaData rsmd = rs.getMetaData();
					int columnCount = rsmd.getColumnCount();
					// 迭代输出ResultSet对象
					while (rs.next())
					{
						// 依次输出每列的值
						for (int i = 0 ; i < columnCount ; i++ )
						{
							System.out.print(rs.getString(i + 1) + "\t");
						}
						System.out.print("\n");
					}
				}
			}
			else
			{
				System.out.println("该SQL语句影响的记录有" 
					+ stmt.getUpdateCount() + "条");
			}
		}
	}
	public static void main(String[] args) throws Exception
	{
		ExecuteSQL es = new ExecuteSQL();
		es.initParam("mysql.ini");
		System.out.println("------执行删除表的DDL语句-----");
		es.executeSql("drop table if exists my_test");
		System.out.println("------执行建表的DDL语句-----");
		es.executeSql("create table my_test"
		+ "(test_id int auto_increment primary key, "
		+ "test_name varchar(255))");
		System.out.println("------执行插入数据的DML语句-----");
		es.executeSql("insert into my_test(test_name) "
		+ "select student_name from student_table");
		System.out.println("------执行查询数据的查询语句-----");
		es.executeSql("select * from my_test");
	}
}

运行上面程序,会看到使用 Statement 的不同方法执行不同 SQL 语句的效果.执行 DDL 语句显示受影响的记录条数为 0;执行 DML 语句显示插入、修改或删除的记录条数;执行查询语句则可以输出查询结果。

上面程序获得 SQL 执行结果时没有根据各列的数据类型调用相应的 getXxx() 方法,而是直接使用 getString() 方法采取得值,这是可以的。ResultSet 的 getString() 方法几乎可以取除 Blob 之外的任意类型列的值,因为所有的数据类型都可以自动转换成字符串类型。

3,使用 PreparedStatement 执行 SQL 语句

如果我们经常需要反复执行一条结构相似的 SQL 语句,例如如下两条 SQL 语句:

insert into student_table values(null , '张三' , 1);
insert into student_table values(null , '李四' , 2);

对于这两条 SQL 语句而言,它们的结构基本相似,只是执行插入时插入的值不同而已。在这种情况下,我们可以使用下面带占位符(?)参数的 SQL 语句来代替它:

insert into student_table values(null , ? , ?);

但 Statement 执行 SQL 语句时不允许使用问号占位符参数,而且这个问号占位符参数必须获得值后才可以执行。为了满足这种功能,JDBC 提供了 PreparedStatement 接口,它是 Statement 接口的子接口,它可以预编译 SQL 语句,预编译后的 SQL 语句被存储在 PreparedStatement 对象中,然后可以使用该对象多次高效地执行该语句。简而言之,使用 PreparedStatement 比使用 Statement 的效率要高。

创建 PreparedStatement 对象使用 Connection 的 prepareStatement() 方法,该方法需要传入一个 SQL 字符串,该 SQL 字符串可以包含占位符参数。如下代码所示:

// 创建一个 PreparedStatement 对象
pstmt = conn.preparedStatement("insert into student_table values(null , ? , 1)");

PreparedStatement 也提供了 execute()、executeUpdate()、executeQuery() 三个方法来执行 SQL 语句,不过这三个方法无须参数,因为 PreparedStatement 已存储了预编译的 SQL 语句。

使用 PreparedStatement 预编译 SQL 语句时,该 SQL 语句可以带占位符参数,因此在执行 SQL 语句之前必须为这些参数传入参数值,PreparedStatement f提供了一系列的 setXxx(int index, Xxx value) 方法来传入参数值。

如果程序很清楚 PreparedStatement 预编译 SQL 语句中各参数的类型,则使用相应的 setXxx() 方法来传入参数即可;如果程序不清楚预编译 SQL 语句中各参赛的类型,则可以使用 setObject() 方法来传入参数,由 PreparedStatement 来负责类型转换。

下面程序示范了使用 Statement 和 PreparedStatement 分别插入 100 条记录的对比。使用 Statement 需要传入 100 条 SQL 语句,但使用 PreparedStatement 则只需要传入 1 条预编译的 SQL 语句,然后 100 次为该 PreparedStaternent 的参数设值即可。

public class PreparedStatementTest
{
	private String driver;
	private String url;
	private String user;
	private String pass;
	public void initParam(String paramFile)throws Exception
	{
		// 使用Properties类来加载属性文件
		Properties props = new Properties();
		props.load(new FileInputStream(paramFile));
		driver = props.getProperty("driver");
		url = props.getProperty("url");
		user = props.getProperty("user");
		pass = props.getProperty("pass");
		// 加载驱动
		Class.forName(driver);
	}
	public void insertUseStatement()throws Exception
	{
		long start = System.currentTimeMillis();
		try(
			// 获取数据库连接
			Connection conn = DriverManager.getConnection(url
				, user , pass);
			// 使用Connection来创建一个Statment对象
			Statement stmt = conn.createStatement())
		{
			// 需要使用100条SQL语句来插入100条记录
			for (int i = 0; i < 100 ; i++ )
			{
				stmt.executeUpdate("insert into student_table values("
					+ " null ,'姓名" + i + "' , 1)");
			}
			System.out.println("使用Statement费时:" 
				+ (System.currentTimeMillis() - start));
		}
	}
	public void insertUsePrepare()throws Exception
	{
		long start = System.currentTimeMillis();
		try(
			// 获取数据库连接
			Connection conn = DriverManager.getConnection(url
				, user , pass);
			// 使用Connection来创建一个PreparedStatement对象
			PreparedStatement pstmt = conn.prepareStatement(
				"insert into student_table values(null,?,1)"))

		{
			// 100次为PreparedStatement的参数设值,就可以插入100条记录
			for (int i = 0; i < 100 ; i++ )
			{
				pstmt.setString(1 , "姓名" + i);
				pstmt.executeUpdate();
			}
			System.out.println("使用PreparedStatement费时:" 
				+ (System.currentTimeMillis() - start));
		}
	}
	public static void main(String[] args) throws Exception
	{
		PreparedStatementTest pt = new PreparedStatementTest();
		pt.initParam("mysql.ini");
		pt.insertUseStatement();
		pt.insertUsePrepare();
	}
}

多次运行上面程序,可以发现使用 PreparedStatement 插入 100 条记录所用的时间比使用 Statement 插入 100 条记录所用的时间少,这表明 PreparedStatement 的执行效率比 Statement 的执行效率高。

除此之外,使用 PreparedStatement 还有一个优势——当 SQL 语句中要使用参数时,无须“拼接” SQL 字符串。而使用 Statement 则要“拼接” SQL 字符串,如上程序中粗体字代码所示,这是相当容易出现错误的——注意代码中的单引号,这是因为 SQL 语句中的字符串用单引号引起来。尤其是当 SQL 语句中有多个字符串参数时,“拼接”这条 SQL 语句时就更容易出错了。使用 PreparedStatement 则只需要使用问号占位符来代替这些参数即可,降低了编程复杂度。

使用 PreparedStatement 还有一个很好的作用——用于防止 SQL 注入。

SQL 注入是一个较常见的 Cracker 入侵方式,它利用 SQL 语句的漏洞来入侵。

下面以一个简单的登录窗口为例来介绍这种 SQL 注入的结果。下面登录窗口包含两个文本框,一个用于输入用户名,一个用于输入密码,系统根据用户输入与 jdbc_test 表里的记录进行匹配,如果找到相应记录则提示登录成功。

public class LoginFrame
{
	private final String PROP_FILE = "mysql.ini";
	private String driver;
	// url是数据库的服务地址
	private String url;
	private String user;
	private String pass;
	// 登录界面的GUI组件
	private JFrame jf = new JFrame("登录");
	private JTextField userField = new JTextField(20);
	private JTextField passField = new JTextField(20);
	private JButton loginButton = new JButton("登录");
	public void init()throws Exception
	{
		Properties connProp = new Properties();
		connProp.load(new FileInputStream(PROP_FILE));
		driver = connProp.getProperty("driver");
		url = connProp.getProperty("url");
		user = connProp.getProperty("user");
		pass = connProp.getProperty("pass");
		// 加载驱动
		Class.forName(driver);
		// 为登录按钮添加事件监听器
		loginButton.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				// 登录成功则显示"登录成功"
				if (validate(userField.getText(), passField.getText()))
				{
					JOptionPane.showMessageDialog(jf, "登录成功");
				}
				// 否则显示"登录失败"
				else
				{
					JOptionPane.showMessageDialog(jf, "登录失败");
				}
			}
		});
		jf.add(userField , BorderLayout.NORTH);
		jf.add(passField);
		jf.add(loginButton , BorderLayout.SOUTH);
		jf.pack();
		jf.setVisible(true);
	}
<pre name="code" class="java">private boolean validate(String userName, String userPass)
	{
		// 执行查询的SQL语句
		String sql = "select * from jdbc_test "
			+ "where jdbc_name='" + userName 
			+ "' and jdbc_desc='" + userPass + "'";
		System.out.println(sql);
		try(
			Connection conn = DriverManager.getConnection(url
				, user ,pass);
			Statement stmt = conn.createStatement();
			ResultSet rs = stmt.executeQuery(sql))
		{
			// 如果查询的ResultSet里有超过一条的记录,则登录成功
			if (rs.next())
			{
				return true;
			}
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
		return false;
	}
public static void main(String[] args) throws Exception{new LoginFrame().init();}}


运行上面程序,如果用户正常输入其用户名、密码当然没有问题,输入正确可以正常登录,输入错误将提示输入失败。但如果这个用户是一个 Cracker,他可以按图 13.21 所示来输入。

图 13.21 所示的输入明显不正确,但当单击“登录”按钮后也会显示“登录成功”对话框。我们还可在程序运行的后台看到如下 SQL 语句:

# 利用 SQL 注入后生成的 SQL 语句
select * from jdbc_test where jdbc_name='' or true or and jdbc_desc=''

看到这条 SQL 语句,读者应该不难明白为什么这样输入也可以显示“正常登录”对话框了,因为 Cracker 直接输入 true,而 SQL 把这个 true 当成了直接量。

JDBC 编程本身并没有提供图形界面功能,它仅仅提供了数据库访问支持.如果希望 JDBC 程序有较好的图形用户界面,则需要结合前面介绍的 AWT 或 Swing 编程才可以做到。在 Web 编程中,数据库访问也是非常重要的基础知识。

如果把上面的 validate() 方法换成使用 PreparedStatement 来执行验证,而不是直接使用 Statement。程序如下:

<pre name="code" class="java">private boolean validate(String userName, String userPass)
{
	try(
		Connection conn = DriverManager.getConnection(url 
			, user ,pass);
		PreparedStatement pstmt = conn.prepareStatement(
			"select * from jdbc_test where jdbc_name=? and jdbc_desc=?"))
	{
		pstmt.setString(1, userName);
		pstmt.setString(2, userPass);
		try(
			ResultSet rs = pstmt.executeQuery())
		{
			//如果查询的ResultSet里有超过一条的记录,则登录成功
			if (rs.next())
			{
				return true;
			}
		}
	}
	catch(Exception e)
	{
		e.printStackTrace();
	}
	return false;
}


将上面的 validate() 方法改为使用 PreparedStatement 来执行 SQL 语句之后,即使用户按图 13.21 所示输入,系绕一样会显示“登录失败”对话框。

总体来看,使用 PreparedStatement 比使用 Statement 多了如下 3 个好处。

PreparedStatement 预编译 SQL 语句,性能更好。

PreparedStatement 无须“拼接” SQL 语句,编程更简单。

PreparedStatement 可以防止 SQL 注入,安全性更好。

基于以上 3 点,通常推荐避免使用 Statement 来执行 SQL 语句,改为使用 PreparedStatement 执行 SQL 语句。

使用 PreparedStatement 执行带占位符参数的 SQL 语句时,SQL 语句中的占位符参数只能代替普通值,不要使用占位符参数代替表名、列名等数据库对象,更不要用占位符参数来代替 SQL 语句中的 insert、select 等关键字。

4,使用 CallableStatement 调用存储过程

下面的 SQL 语句可以在 MySQL 数据库中创建一个简单的存储过程。

delimiter //
create procedure add_pro(a int , b int , out sum int)
begin
set sum = a + b;
end;
//

上面的 SQL 语句将 MySQL 的语句结束符改为双斜线(//),这样就可以在创建存储过程中使用分号作为分隔符(MySQL 默认使用分号作为语句结束符)。上面程序创建了名为 add_pro 的存储过程,该存储过程包含 3 个参数:a、b 是传入参数,而 sum 使用 out 修饰,是传出参数。

关于存储过程的介绍请读者自行查阅相关书籍。

调用存储过程使用 CallableStatcment,可以通过 Connection 的 prepareCall() 方法来创建 CallableStatement 对象,创建该对象时需要传入调用存储过程的 SQL 语句。调用存储过程的 SQL 语句总是这种格式:{ call 过程名(?,?,?...)},其中的问号作为存储过程参数的占位符。例如,如下代码就创建了调用上面存储过程的 CallableStatement 对象。

// 使用 Connection 来创建一个 CollableStatement 对象
cstmt = conn.prepareCall("{call add_pro(?,?,?)}");

存储过程的参数既有传入参数,也有传出参数。所谓传入参数就是Java程序必须为这些参数传入值,可以通过 CallableStatement 的 setXxx() 方法为传入参数设置值。所谓传出参数就是 Java 程序可以通过该参数获取存储过程里的值,CallableStatement 需要调用 registerOutParameter() 方法来注册该参数。如下代码所示:

// 注册 CallableStatement 的第三个参数是 int 类型
cstmt.registerOutParameter(3 , Types.INTEGER);

经过上面步骤之后,就可以调用 CallableStatement 的 execute() 方法来执行存储过程了,执行结束后通过 CallableStatement 对象的 getXxx(int index) 方法来获取指定传出参数的值。下面程序示范了如何来调用诙存储过程。

public class CallableStatementTest
{
	private String driver;
	private String url;
	private String user;
	private String pass;
	public void initParam(String paramFile)throws Exception
	{
		// 使用Properties类来加载属性文件
		Properties props = new Properties();
		props.load(new FileInputStream(paramFile));
		driver = props.getProperty("driver");
		url = props.getProperty("url");
		user = props.getProperty("user");
		pass = props.getProperty("pass");
	}
	public void callProcedure()throws Exception
	{
		// 加载驱动
		Class.forName(driver);
		try(
			// 获取数据库连接
			Connection conn = DriverManager.getConnection(url
				, user , pass);
			// 使用Connection来创建一个CallableStatment对象
			CallableStatement cstmt = conn.prepareCall(
				"{call add_pro(?,?,?)}"))
		{
			cstmt.setInt(1, 4);
			cstmt.setInt(2, 5);
			// 注册CallableStatement的第三个参数是int类型
			cstmt.registerOutParameter(3, Types.INTEGER);
			// 执行存储过程
			cstmt.execute();
			// 获取,并输出存储过程传出参数的值。
			System.out.println("执行结果是: " + cstmt.getInt(3));
		}
	}
	public static void main(String[] args) throws Exception
	{
		CallableStatementTest ct = new CallableStatementTest();
		ct.initParam("mysql.ini");
		ct.callProcedure();
	}
}

上面程序中 try 代码块就是执行存储过程的关键代码,运行上面程序将会看到这个简单存储过程的执行结果,传入参数分别是 4、5,执行加法后传出总和 9。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值