JDBC进行数据库操作
Statement:用于执行静态SQL语句
PreparedStatement:SQL语句被预编译在此对象中,可以多次高效的使用这段语句。
CallableStatement:用于执行SQL存储过程
Statement也会因为Java语言和写法问题产生依赖注入问题,导致查询语句的WHERE条件恒成立。
使用PreparedStatement对数据库实现增删改操作
一条完整的使用PreparedStatement实现插入操作的完整代码如下:
- 读取配置文件
- 加载驱动,获取连接
- 预编译SQL语句(使用prepareStatement)
- 插入数据
- 进行try-catch进行资源关闭
@Test
public void testInsert() {
Connection conn = null;
PreparedStatement ps = null;
try {
//读取配置文件
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
//加载驱动
Class.forName(driverClass);
//获取连接
conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
//预编译SQL语句
String sql = "insert into customers(name, email, birth)values(?, ?, ?)";
ps = conn.prepareStatement(sql);
//填充占位符
ps.setString(1, "太宰治");
ps.setString(2, "zhi@qq.com");
//对于时间的获取
//建立对于时间的格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
java.util.Date date = sdf.parse("2050-10-02"); //使用格式建立具体的时间,使用getTime()方法获取时间戳
ps.setDate(3, new Date(date.getTime())); //注意引入的包为java.sql下的Date
ps.execute(); //执行操作
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(ps != null){
ps.close(); //资源关闭
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if(conn != null){
conn.close(); //连接关闭
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
可以将某些重复的操作封装到方法中,在使用时直接调用:
将获取数据库连接操作封装
public static Connection getConnection() throws Exception {
//读取配置文件
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
//加载驱动
Class.forName(driverClass);
//获取连接
Connection conn = DriverManager.getConnection(url, user, password);
return conn;
}
将资源关闭操作封装
public void closeResource(Connection conn, Statement state){
try {
if(state != null){
state.close(); //资源关闭
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if(conn != null){
conn.close(); //连接关闭
}
} catch (SQLException e) {
e.printStackTrace();
}
}
使用封装好的工具类实现修改数据的操作
@Test
public void testUpdate() throws Exception {
Connection conn = null;
PreparedStatement ps = null;
try {
//获取连接
conn = JDBCutils.getConnection();
//预编译SQL
String sql = "update customers set name = ? where id = ?";
ps = conn.prepareStatement(sql);
//填充占位符
ps.setObject(1, "江户川乱步");
ps.setObject(2, 19);
ps.execute();
} finally {
JDBCutils.closeResource(conn, ps);
}
}
将增删改操作封装为统一的方法,使用可变形参列表进行实现。
//进行测试调用
@Test
public void testCommonTest(){
// String sql = "delete from customers where id = ?";
// update(sql, 19);
String sql = "update `order` set order_name = ? where order_id = ?";
update(sql, "DD", "2");
}
//通用操作的定义
public void update(String sql, Object ...args){
Connection conn = null;
PreparedStatement ps = null;
try {
conn = JDBCutils.getConnection();
ps = conn.prepareStatement(sql);
for(int i = 0; i < args.length; i++){
ps.setObject(i + 1, args[i]);
}
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCutils.closeResource(conn, ps);
}
}
进行查询操作的方法
使用ORM编程思想
- 将一个数据库表视作一个java类
- 将一个数据库记录视作一个具体的java对象
- 将每个对象的每个字段视为一个java对象中的一个属性
注意:查询操作与增删改不同,查询操作需要接收返回值,我们使用ResultSet类型的变量来接收返回值,并通过.executeQuery()方法来进行操作。
注意:ResultSet也需要进行关闭。
下为一个查询操作的基础实现,其中忽略了javabean的编写。
@Test
public void testQuery() {
Connection conn = null;
PreparedStatement ps = null;
ResultSet resultSet = null;
try {
//获得连接(使用创建的工具类)
conn = JDBCutils.getConnection();
//预编译sql
String sql = "select id, name, email, birth from customers where id = ?";
ps = conn.prepareStatement(sql);
//填充占位符
ps.setObject(1, 4);
//保存查询结果
resultSet = ps.executeQuery();
//将查询结果转换为java属性
if(resultSet.next()){
int id = resultSet.getInt(1); //获取第一个数据
String name = resultSet.getString(2);
String email = resultSet.getString(3);
Date birth = resultSet.getDate(4); //引入sql下的Date
//使用JAVA对象,即ORM编程思想创建对象,此处的Customer是对应customers表的javabean
Customer customer = new Customer(id, name, email, birth);
System.out.println(customer);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭资源
JDBCutils.closeResource(conn, ps, resultSet);
}
}
进阶编写查询:
第一种方法中,sql要在代码中实现,不能封装为方法,下提供将sql封装为方法的代码实现:
将sql语句和传入的参数封装为形参,将查询封装为方法。
@Test
public void testQueryForCustomers(){
String sql = "select id, birth, email, name from customers where id = ?";
Customer cust = QueryForCustomers(sql, 18);
System.out.println(cust);
}
public Customer QueryForCustomers (String sql, Object...args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCutils.getConnection();
ps = conn.prepareStatement(sql);
for(int i = 0; i < args.length; i++){
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
//注意只有在云数据中才可以知道rs的列数,使用.getMetaData()来获取元数据
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount(); //.getColumnCount()方法来获取列数
if(rs.next()){
Customer cos = new Customer();
for(int i = 0; i < columnCount; i++){
Object columnvalue = rs.getObject(i + 1);
//获取列名
String columnName = rsmd.getColumnName(i + 1);
//利用反射动态的给Customer的实例对象赋值
Field field = Customer.class.getDeclaredField(columnName); //.getDeclaredField()获取字段中指定的属性,不包括父类
field.setAccessible(true); //令反射可以修改私有化的属性
field.set(cos, columnvalue);
}
return cos;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCutils.closeResource(conn, ps, rs);
}
return null;
}
注意:结果集中的数据是存储在ResultSet中的,结果集的属性(列名、列数等)是存储在ResultSetMetaData中的(使用.getMetaData()方法获取)
对于列名与JavaBean中属性名不一致时
.getColumnName()获取列的列名(不受别名影响)
.getColumnLabel()获取列的列名(受别名影响)
故一定要起别名,与JavaBean中的属性名相对应
通过反射在运行时类中赋值。
@Test
public void testQueryForOrder(){
String sql = "select order_id as orderId, order_name as orderName, order_date as orderDate from `order` where order_id = ?";
Order order = orderForQuery(sql, 2);
System.out.println(order);
}
public Order orderForQuery(String sql, Object...args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCutils.getConnection();
ps = conn.prepareStatement(sql);
for(int i = 0; i < args.length; i++){
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
if(rs.next()){
Order order = new Order();
for(int i = 0; i < columnCount; i++){
Object columnValue = rs.getObject(i + 1); //获取数据
// String columnName = rsmd.getColumnName(i + 1); //获取数据库中的列名
String columnLabel = rsmd.getColumnLabel(i + 1);
Field field = Order.class.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(order, columnValue);
}
return order;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCutils.closeResource(conn, ps, rs);
}
return null;
}
针对不同表的通用查询方法
使用泛型与反射动态的将表的获取通用化,使传入表名的运行时类就可以自动实现对不同表的读取。
@Test
public void testQuery(){
String sql = "select order_id as orderId, order_name as orderName, order_date as orderDate from `order` where order_id = ?";
Order order = QueryForCustomers(Order.class, sql, 2);
System.out.println(order);
}
public <T> T QueryForCustomers (Class<T> clazz, String sql, Object...args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCutils.getConnection();
ps = conn.prepareStatement(sql);
for(int i = 0; i < args.length; i++){
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
//注意只有在云数据中才可以知道rs的列数,使用.getMetaData()来获取元数据
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount(); //.getColumnCount()方法来获取列数
if(rs.next()){
T t = clazz.newInstance();
for(int i = 0; i < columnCount; i++){
Object columnvalue = rs.getObject(i + 1);
//获取列名
String columnName = rsmd.getColumnLabel(i + 1);
//利用反射动态的给Customer的实例对象赋值
Field field = clazz.getDeclaredField(columnName); //.getDeclaredField()获取字段中指定的属性,不包括父类
field.setAccessible(true); //令反射可以修改私有化的属性
field.set(t, columnvalue);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCutils.closeResource(conn, ps, rs);
}
return null;
}
针对于不同表,多条数据的通用查询方法
使用ArrayList列表来存储查询到的多条数据。
@Test
public void testQuery(){
String sql = "select id, name, email from customers where id < ?";
List list = getForList(Customer.class, sql, 18);
list.forEach(System.out::println);
}
public <T> List<T> getForList(Class<T> clazz, String sql, Object...args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCutils.getConnection();
ps = conn.prepareStatement(sql);
for(int i = 0; i < args.length; i++){
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
//注意只有在云数据中才可以知道rs的列数,使用.getMetaData()来获取元数据
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount(); //.getColumnCount()方法来获取列数
ArrayList<T> list = new ArrayList<T>();
while(rs.next()){
T t = clazz.newInstance();
for(int i = 0; i < columnCount; i++){
Object columnvalue = rs.getObject(i + 1);
//获取列名
String columnLabel = rsmd.getColumnLabel(i + 1);
//利用反射动态的给Customer的实例对象赋值
Field field = clazz.getDeclaredField(columnLabel); //.getDeclaredField()获取字段中指定的属性,不包括父类
field.setAccessible(true); //令反射可以修改私有化的属性
field.set(t, columnvalue);
}
list.add(t);
}
return list;
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCutils.closeResource(conn, ps, rs);
}
return null;
}
为什么PreparedStatement可以解决SQL注入问题
Statement不会预编译SQL,会在字符串拼接完成后直接执行SQL语句,在SQL语句的写法出现问题时会执行有问题的语句。
PreparedStatement会预编译SQL,逻辑关系在完整的SQL语句填充之前就确定了,只是利用占位符,将需要的信息补齐,故可以避免SQL语句问题导致的SQL注入。
另外,PreparedStatement可以利用占位符传输流数据(Blob类型的数据,非常大),而Statement作为SQL语言的连接器不可以。
PreparedStatement也实现了更为高效的批量操作,Statement需要对于每一条SQL进行SQL校验,PreparedStatement进行预编译之后就只需要进行第一次的SQL校验。