前言
公司上层决定逐渐将目前使用的c#+SqlServer后端模式逐渐转向Java+MySql模式。估计很长一段时间内两种模式会并行存在。c#+SqlServer虽然开发效率高,我们也比较熟练,但是授权成本也不容忽视。Java 开源+MySql近几年已经发展的非常成熟,开发工具非常齐全, 而且有些客户还指定要求java版的后端。 本人虽然早年接触过Java,但时间已经久远,忘得也差不多了。这次要重新开始学习Java基础。也比较下和c#语言的区别。
学习Java
网上有不少教程可以参考,现在网络时代就是方便,以前还得出去买书看。教程里面讲的都是比较基础的知识, 感觉和c#很相似,各种对应的概念都有只是写法不同。这里熟悉下就好,具体写的时候再仔细查吧。再看看java的新特性,似乎java也支持lamda表达式了
IDE工具选择
早年记忆力是Java就用Eclipse,看来现在又出来不少好工具。这里就还是用Eclipse吧。以后可以移植到其他IDE比较下优劣, 到底不用不知道么, 正好教程里就有Eclipse的下载链接 http://www.eclipse.org/downloads/packages/ 赶快下下来。
我下的叫 Eclipse IDE for Java Developers 2022-12 版。这个似乎自带最新的java环境。这次只是研究就不考虑Java版本,尽量不要用太新的语法就好。到底国内开发基本都是1.8,也就是Java8。
系统环境选择
这次系统环境就选Windows系统了,Linux也是开发起来也差不多,差别最多就是环境变量配置方式,路径分隔符之类的。
开发框架选择
Java最近流行的开发框架就是Spring boot了,支持Java8-17。这次就选择这个吧。C#的话开发框架都是Visual Studio IDE自带的,省心省力但不省钱。
安装配置Spring boot [jdk + maven/gradle + sts]
JDK:Eclipse 自带的 java17, 也可以配置自己的
Maven:包管理器,Eclipse 自带, 也可以配置本地版
gradle: 包管理器,Eclipse 自带,也可以配置本地版
上面三个Eclipse都自带了,可以进行些个性化配置, 比如Jdk版本, 包下载地址, 镜像源之类的。我这里下载包编译包和运行工程似乎都没啥问题,现在能用就好了
STS:Eclipse 的Spring boot 插件
eclipse > help >Eclipse marketplace 耐心等待。。点击 Popular, 安装第一个即可。我这里已经是Installed
接下来就是等待安装我这里装的比较慢,可能会跳出同意信任啥啥的,反正都同意就好了。
完成后 File > New > other 出现这个Spring Boot > Spring Starter Project 就算装好了
到这里算是开发环境算是有了, 哎休息休息继续写。
开始第一个Demo项目
点击上图的Next, 如下配置,默认的总是最好的,包管理就用Gradle吧,不折腾
选择组件, 第一次选择分别在 Developer Tool, SQL, Web里,数据库就先用现成的SqlServer了,这次改造数据库不迁移。
用MySql, Oracle 等就是数据源配置有些变动,代码变动估计不大。
点击Finish, 开始拼网速吧。慢慢等待。。如果安装过的再装似乎就快了。所以库缓存可能还是别放在C盘好,还好可以修改的
完成后可以看到导航栏出现了一个项目, 有点样子了
终于第一个项目建成了, 不容易。
项目建立好了,首先是建立代码管理, 这里就用GIT。不建也可以,但是后悔的时候很痛苦
工程目录
git init
git add -A
git commit -am '建立项目'
spring boot 建立时里面已经有了.gitignore文件,这倒是省却了我们自己整理
如果有远程库也可以推送下
DAO架构移植
接下来尝试把老的c# dao 改为 java dao。 将使用相同的分成架构, 顺便看看性能差异。有以下4层。简单的dao虽然也可以用Mybiatis,这次主要是学习目的。java到底写起来如何得感受下。
Mybiatis下次专门学习下,似乎蛮流行的
Controller层:这里写Api调用
Service层: 这里写业务逻辑
Repository层:这里写数据库逻辑
Storage层: 这里写数据库交互,这里的sql可能对每种数据库会有稍微不同
层数是根据具体项目定的,差不多就行,多一个层就多一个实体类了
先建立个测试表, 数据库先用 Sql Server
USE [java_demo]
GO
/****** Object: Table [dbo].[user_info] Script Date: 2023/3/1 10:59:43 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[user](
[id] [int] IDENTITY(1,1) NOT NULL,
[account] [varchar](50) NULL,
[name] [nvarchar](50) NULL,
[status] [int] NULL
) ON [PRIMARY]
GO
先随便建一个,接下来就要开始写java了
建立java前台实体类 data to object , 就用经典的UserDTO
不过少了get set 似乎不完整, c# 可以直接写 int id? {get; set;}, 还好java现在有了lombok,以前只能写getId() setId() 等方法, 代码量会很大。
改造UserDTO
以上是使用lombok 代码修改后的样子
追加Controller文件夹内, 新建一个UserController控制器
右键 demo.java.dao > New > Folder
右键 controller > New > Class
然后新项目的目录显示有点问题要设置下
这是目录同步显示内容, 需要的可以激活
点击工程根目录 ctrl+F10 > Package Presentation > Heirarchical 这样目录就不会平行显示了..
实现一个简单的Get 请求,看看java怎么写 api
第一个程序当然是hello了, 可以看到setName方法不存在, 不是说好的lombok会自动搞定的?
查了以下似乎需要到eclipse安装目录里配置lombok生成器。
打开目录 eclipse \ eclipse.ini 文件, 末尾追加两横配置
-Xbootclasspath/a:lombok.jar
-javaagent:lombok.jar
这因该是说使用这个lombok.jar 生成,这个lombok.jar要到Repository里找
以看到在这个文件下有个lombok-1.18.26.jar
lombok-1.18.26.jar 改名为 lombok.jar 复制到 eclipse \ eclipse.ini 同级目录, 重启eclipse
似乎可以了,总算没有报错。这个lombok似乎有点用,只是他会生成一堆代码,如果心里没底可能会发生意外,还有就是侵入性较强,依赖也大,有时候代码提示会卡顿。但是getXX setXX在java里建立又是非常基本的操作,c#里这个叫Property 属性。这是一种内外分离解耦的流行模式, 这种get set其实找个工具生成也能达到类似效果,所以实际工程感觉我不太会用吧。
nodejs版 get set 脚本
let splits = input.trim().split('\n');
let results = [];
for (const line of splits) {
if (line.indexOf('@') == -1 && line.indexOf('*') == -1 && line.indexOf('/') == -1) {
let words = line.trim().replace(';', '').split(' ');
if (words.length > 2) {
let type = words[1];
let name = words[2];
if (name) {
let prop = name.charAt(0).toUpperCase() + name.substring(1);
results.push(
`public ${type} get${prop}() {
return this.${name};
}
public void set${prop}(${type} val) {
this.${name} = val;
}
`
);
}
}
}
}
return results.join('\n');
复制这段
运行小脚本
public Integer getId() {
return this.id;
}
public void setId(Integer val) {
this.id = val;
}
public String getAccount() {
return this.account;
}
public void setAccount(String val) {
this.account = val;
}
public String getName() {
return this.name;
}
public void setName(String val) {
this.name = val;
}
public Integer getStatus() {
return this.status;
}
public void setStatus(Integer val) {
this.status = val;
}
这样代码都有了。这种方法也不多花时间,就是看起来代码不简洁。这种小脚本用什么写都可以的
python版 get set 脚本
import pyperclip as cb
input = cb.paste()
lines = input.splitlines(False)
results = []
for line in lines:
if line.find('@') == -1 and line.find('*') == -1 and line.find('/') == -1:
words = line.strip().replace(';', '').split(' ')
if len(words) > 2:
type = words[1]
name = words[2]
if name:
data = {"name": name, "type": type, "prop": name[0].upper() + name[1:]}
results.append("""public {type} get{prop}() {{
return this.{name};
}}
public void set{prop}({type} val) {{
this.{name} = val;
}}
""".format(**data))
cb.copy(''.join(results))
Python就是要用format函数比较老套点,其他倒是比较简洁的 ,取得剪切板也简单
第一次运行
运行下试试看 这里先 项目 > Run AS > Spring Boot App
结果就失败了
原来是我用了数据库包,但是没有配置数据库连接,所以它拒绝给我启动。我感觉设计师的想法很超前,我还没写到数据库不是么
打开 application.properties 配置文件 追加以下配置
spring.datasource.url=jdbc:sqlserver://xxx.xxx.xxx.xxx:1433;DatabaseName=java_demo;encrypt=true;trustServerCertificate=true
spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.datasource.username=sa
spring.datasource.password=xxxxx
数据库地址和密码根据实际改下就好, 再次启动
异常没有了, 执行 localhost:8080/hello?name=world
似乎成功了,setName也起了作用, 不错不错。
service层文件夹 和 UserSerivicece.java
这里准备实现一个create方法用来插入数据, dto是数据, 返回的dto里会带有自增列的值
service层的自动注入
一般设计来说service是注入进来的,而不是每次手动的new UserSercice()
springboot里已经提供了类似的方案, @Autowired 标签
修改UserService.java
修改UserController.java
运行试试看
curl 请求
curl http://localhost:8080/user/create -H "Content-Type: application/json" -X POST -d '{
"id": 0,
"account": "admin",
"name": "管理员",
"status": "0"
}'
结果很奇怪, 除了Id以外没有值
修改post data, 所有值改为首字母大写
curl http://localhost:8080/user/create -H "Content-Type: application/json" -X POST -d '{
"Id": 0,
"Account": "admin",
"Name": "管理员",
"Status": "0"
}'
这次成功了,似乎spring boot 这里是区分大小写的,这里和c#项目不同。可能是性能考虑吧。
返回值追加一个Response包含类, 如Response<UserDTO>
一般写结构都会包一个Response,这里也来模仿下,这里需要用到java的泛型。看起来和C#差不多少
WebResponse很简单
这个response很简单,这里就不扩展了
修改controller层返回值
测试下试试
curl http://localhost:8080/user/create -H "Content-Type: application/json" -X POST -d '{
"Id": 0,
"Account": "admin",
"Name": "管理员",
"Status": "0"
}'
好了,包含在data里了,以后可以加消息,状态等属性。
Repository层实现
同样的加一个UserRepository类
service层对应修改
Repositroy是可以独立于Service运行的,所以他们很像。
这一层是数据中间层, 一般用来切换不同的数据库实现。如果数据库实现是相同 的,或者只有一个数据库,视情况这层也可以合并为Service层。
运行下试试
返回了1002, 看来成功了。
Storage层实现
终于可以写SQL层了,写一个传统的多层DAO如果没有啥特殊逻辑是比较枯燥的一件事,所以才有了各种简化的方案,不就是往数据库里插点东西这么麻烦是不是很累啊。
同理 建立 storage 文件夹和UserStorage类
repository也修改下
一层又是一层啊,先测试下
好返回了1003, 继续层
数据库插入的实现
现在要想办法把数据插入在准备好的user表里
数据库插入的实现方案有许多,总之先使用最原始的方案,学习学习
粗略写好了,先不上代码,先跑跑看吧,结果果然的报错了
看来是数据库连接失败,不因该啊。查了下有好多解答,看来大家都遇到这问题啊,大体意思就是服务器的协议已经不安全了我禁用了,说到底这个测试的sql server太老了。这种问题发布到客户时再考虑也可以的,现在先解除禁用吧。
JDK 根目录 /lib/security/java.security
搜索 jdk.tls.disabledAlgorithms 找到类似这句
jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5withRSA, \
DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL
把 TLSv1, TLSv1.1 都删掉
jdk.tls.disabledAlgorithms=SSLv3, RC4, DES, MD5withRSA, \
DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL
再试试看
curl http://localhost:8080/user/create -H "Content-Type: application/json" -X POST -d '{
"Id": 0,
"Account": "admin",
"Name": "管理员",
"Status": "0"
}'
哦哦成功了!看似很快搞定的 ,其实还是调了一会的, 低级错误到处有。
再次发送
看来自增运行的不错
直接看数据库也没问题
以下是Insert代码
public UserDTO insert(UserDTO dto) {
String driverName = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
String dbURL = "jdbc:sqlserver://xxx.xxx.xxx.xxx:1433;DatabaseName=java_demo;encrypt=true;trustServerCertificate=true";
String userName = "sa";
String userPwd = "xxxxx";
Connection dbConn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 注册驱动
Class.forName(driverName);
// 获取数据库连接
dbConn = DriverManager.getConnection(dbURL, userName, userPwd);
//System.out.println("连接数据库成功");
// user 是sql关键字似乎不能直接用,看来实际使用时要避免这个名字
ps = dbConn.prepareStatement("insert into [user](account,name,status) values(?,?,?)", Statement.RETURN_GENERATED_KEYS);
ps.setString(1, dto.getAccount());
ps.setString(2, dto.getName());
ps.setInt(3, dto.getStatus());
ps.execute();
//取得自增
rs = ps.getGeneratedKeys();
if (rs.next()) {
// 取得自增
dto.setId(rs.getInt(1));
}
//尽快释放
rs.close();
ps.close();
dbConn.close();
} catch (Exception e) {
e.printStackTrace();
// 错误先简单返回null
dto = null;
} finally {
// 最后一搏
if(rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(dbConn != null) {
try {
dbConn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return dto;
}
可以看到原始未封装的sql插入十分的繁琐,主要是再申请连接和释放资源。这里错误时先简单返回null,也可以改造为用WebResponse返回自定义错误信息。一般返回值结构项目都有统一规定的。
项目目录整理
既然第一个insert成功了,项目也该整理以下。
简化数据插入之Mybatis
现在似乎流行Mybatis,而且上手简单,那就先试试这个吧
首先是 追加 Mybatis Framework
要用的都选上,单个添加似乎不行,因为会警告依赖变少的, 感觉有点怪。有提示就凑合吧
耐心等待。。
先追加一个MybatisUtil类
public class MybatisUtil {
// 先设为私有吧
private static SqlSessionFactory sqlSessionFactory;
// 静态工厂初始化,这个会最先执行
static {
try {
String resource = "mybatis-config.xml";
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream(resource));
} catch (IOException e) {
// 处理掉异常,这样失败的话sqlSessionFactory 就是null了
// 一般是需要个日志,这里就简化了
e.printStackTrace();
}
}
// 简化session取得
public static SqlSession openSession(){
return sqlSessionFactory.openSession();
}
}
这个就是mybaits的工具类了, 看代码就知道还需要个配置文件
配置内容
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置参数文件 -->
<properties resource="application.properties"/>
<environments default="sqlserver">
<environment id="sqlserver">
<!-- 配饰驱动为JDBC -->
<transactionManager type="JDBC"/>
<!-- 连接池配置 -->
<dataSource type="POOLED">
<property name="driver" value="${spring.datasource.driverClassName}"/>
<property name="url" value="${spring.datasource.url}"/>
<property name="username" value="${spring.datasource.username}"/>
<property name="password" value="${spring.datasource.password}"/>
</dataSource>
</environment>
</environments>
<!-- 配置映射资源文件 -->
<mappers>
<mapper resource="demo/java/api/dao/batis/mapper/userMapper.xml"/>
</mappers>
</configuration>
数据库配置在 application.properties 里,这次用上了
dao下追加batis文件夹目录结构, 最佳UserMapper.xml 和 UserBatis.java接口文件
这次使用的是Mybatis的接口模式
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace 填写绑定的interface-->
<mapper namespace="demo.java.api.dao.batis.UserBatis">
<!--id 填写绑定的方法名 写下来是 参数类型名 返回自增true 自增key设置-->
<insert id="insert" parameterType="demo.dto.UserDTO" useGeneratedKeys="true" keyProperty="id">
insert into [user](account,name,status) values(#{account},#{name},#{status})
</insert>
</mapper>
sql server 和 mysql这种支持支持自增的这样写就可以了,oracle的话虽说也可以支持自增,配起来会复杂点,也都能搞定。
可以看到都是些反射需要的配置参数, 这个mapper是需要注册的,如下配置
UserBatis.java
public interface UserBatis {
public void insert(UserDTO dto);
}
只有一个insert方法
UserDAO追加一个 insertByBatis方法
public UserDTO insertByBatis(UserDTO dto) {
SqlSession session = null;
try {
// 取得session,感觉这句比较费时,不知道有没有缓存
session = MybatisUtil.openSession();
// 取得绑定的insert, 所以xml里namespace要写的一样
UserBatis userBatis = session.getMapper(UserBatis.class);
// 执行
userBatis.insert(dto);
// 签入数据 不签入则会浪费自增列
session.commit();
} catch (Exception e) {
// 错误时dto为null
dto = null;
e.printStackTrace();
} finally {
// 关闭sqlSession
session.close();
}
return dto;
}
可以看到这个比原生的inset简单了许多, 当然配置xml也是要时间的
修改repository层实现
repository本来就是用来切换数据库的,这里就简单的改代码实现吧
那好都搞定了就马上试试看。
curl http://localhost:8080/user/create -H "Content-Type: application/json" -X POST -d '{
"Id": 0,
"Account": "admin",
"Name": "管理员",
"Status": "0"
}'
可以看到运行成功了, id也到14, 来看看数据库
可以看到缺了不少id。这是因为忘了写 session.commit(); 不 commit 是不会产生异常的,所以代码要好好测试啊。
改变下参数试试
curl http://localhost:8080/user/create -H "Content-Type: application/json" -X POST -d '{
"Id": 0,
"Account": "admin1",
"Name": "管理员1",
"Status": "1"
}'
目前看起来Mybatis还是不错的, 我觉的Mybatis的魅力是分离数据库实现,这样我把Sql改为oracle实现,mysql实现都不用重新编译。缺点么就是类名的绑定方面需要手动改,如果改了包名对应的配置都要改。对于成熟的项目架构这也不是问题。还有么就是,这都是反射实现的不知道性能代价如何了,需要看实际的测试了。
使用JdbcTemplate插入数据
Mybatis还有许多功能没有挖掘, 如果只是一个反射器那谁都会写了,它的高级功能就先不研究了,首先继续简化Jdbc,这次使用的是jdbcTemplate,目前这个也是比较流行的。
@Autowired 自动创建的jdbcTemplate会自动读取application.properties里的配置
修改 UserDao.java 追加如下内容
public class UserDAO {
// 数据库对象使用自动注入
@Autowired
private JdbcTemplate jdbcTemplate;
// 使用JdbcTemplate对象插入
public UserDTO insertByTemplate(UserDTO dto) {
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
var ps = conn.prepareStatement("insert into [user](account,name,status) values(?,?,?)", new String[] { "id" });
ps.setString(1, dto.getAccount());
ps.setString(2, dto.getName());
ps.setInt(3, dto.getStatus());
return ps;
}
}, keyHolder);
// 不是自增列不能用會異常
dto.setId(keyHolder.getKey().intValue());
return dto;
}
。。。。。。。
}
可以看到这里没有用try catch包含,jdbc也不推荐使用try chath,如果不处理异常信息就会抛弃到前台,如果要处理可以追加全局异常捕获
简单测试异常下会如何,手动创建bug
运行
看来返回了一个500错误,这样前台应该会走错误处理。如果这个返回值不满意也可以全局捕获整理然后输出
修改UserRepository.java
修复bug运行下试试
可以看到id到了18
很好数据库也成功了,发现JdbcTemplate不用close清理资源,而且不不用try catch,代码量也因此大幅减少
使用字段名来指定jdbc的statment参数
写c#时都是名称绑定参数的,jdbc的参数绑定却是index。简单的intsert倒是还好,复杂的select就要混乱了,还是想办法支持名称绑定,适当牺牲一点性能也是必要的
网上流传的某大神的作品,听说可以达到目的
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* This class wraps around a {@link PreparedStatement} and allows the programmer to set parameters by name instead of by
* index. This eliminates any confusion as to which parameter index represents what. This also means that rearranging
* the SQL statement or adding a parameter doesn't involve renumbering your indices. Code such as this:
*
*
* Connection con=getConnection(); String query="select * from my_table where name=? or address=?"; PreparedStatement
* p=con.prepareStatement(query); p.setString(1, "bob"); p.setString(2, "123 terrace ct"); ResultSet
* rs=p.executeQuery();
*
* can be replaced with:
*
* Connection con=getConnection(); String query="select * from my_table where name=:name or address=:address";
* NamedParameterStatement p=new NamedParameterStatement(con, query); p.setString("name", "bob"); p.setString("address",
* "123 terrace ct"); ResultSet rs=p.executeQuery();
*
* Sourced from JavaWorld Article @ http://www.javaworld.com/javaworld/jw-04-2007/jw-04-jdbc.html
*
* @author adam_crume
*/
public class NamedParameterStatement {
/** The statement this object is wrapping. */
private final PreparedStatement statement;
/** Maps parameter names to arrays of ints which are the parameter indices. */
private Map<String, int[]> indexMap;
/**
* Creates a NamedParameterStatement. Wraps a call to c.{@link Connection#prepareStatement(java.lang.String)
* prepareStatement}.
*
* @param connection
* the database connection
* @param query
* the parameterized query
* @throws SQLException
* if the statement could not be created
*/
public NamedParameterStatement(Connection connection, String query) throws SQLException {
String parsedQuery = parse(query);
statement = connection.prepareStatement(parsedQuery);
}
/**
* Creates a NamedParameterStatement. Wraps a call to c.{@link Connection#prepareStatement(java.lang.String)
* prepareStatement}.
*
* @param connection
* the database connection
* @param query
* the parameterized query
* @param columnNames
* key columns
* @throws SQLException
* if the statement could not be created
*/
public NamedParameterStatement(Connection connection, String query, String columnNames[]) throws SQLException {
String parsedQuery = parse(query);
statement = connection.prepareStatement(parsedQuery, columnNames);
}
/**
* Parses a query with named parameters. The parameter-index mappings are put into the map, and the parsed query is
* returned. DO NOT CALL FROM CLIENT CODE. This method is non-private so JUnit code can test it.
*
* @param query
* query to parse
* @param paramMap
* map to hold parameter-index mappings
* @return the parsed query
*/
final String parse(String query) {
// I was originally using regular expressions, but they didn't work well for ignoring
// parameter-like strings inside quotes.
int length = query.length();
StringBuffer parsedQuery = new StringBuffer(length);
boolean inSingleQuote = false;
boolean inDoubleQuote = false;
int index = 1;
HashMap<String, List<Integer>> indexes = new HashMap<String, List<Integer>>(10);
for (int i = 0; i < length; i++) {
char c = query.charAt(i);
if (inSingleQuote) {
if (c == '\'') {
inSingleQuote = false;
}
} else if (inDoubleQuote) {
if (c == '"') {
inDoubleQuote = false;
}
} else {
if (c == '\'') {
inSingleQuote = true;
} else if (c == '"') {
inDoubleQuote = true;
} else if (c == ':' && i + 1 < length && Character.isJavaIdentifierStart(query.charAt(i + 1))) {
int j = i + 2;
while (j < length && Character.isJavaIdentifierPart(query.charAt(j))) {
j++;
}
String name = query.substring(i + 1, j);
c = '?'; // replace the parameter with a question mark
i += name.length(); // skip past the end if the parameter
List<Integer> indexList = indexes.get(name);
if (indexList == null) {
indexList = new LinkedList<Integer>();
indexes.put(name, indexList);
}
indexList.add(Integer.valueOf(index));
index++;
}
}
parsedQuery.append(c);
}
indexMap = new HashMap<String, int[]>(indexes.size());
// replace the lists of Integer objects with arrays of ints
for (Map.Entry<String, List<Integer>> entry : indexes.entrySet()) {
List<Integer> list = entry.getValue();
int[] intIndexes = new int[list.size()];
int i = 0;
for (Integer x : list) {
intIndexes[i++] = x.intValue();
}
indexMap.put(entry.getKey(), intIndexes);
}
return parsedQuery.toString();
}
/**
* Returns the indexes for a parameter.
*
* @param name
* parameter name
* @return parameter indexes
* @throws IllegalArgumentException
* if the parameter does not exist
*/
private int[] getIndexes(String name) {
int[] indexes = indexMap.get(name);
if (indexes == null) {
throw new IllegalArgumentException("Parameter not found: " + name);
}
return indexes;
}
/**
* Sets a parameter.
*
* @param name
* parameter name
* @param value
* parameter value
* @throws SQLException
* if an error occurred
* @throws IllegalArgumentException
* if the parameter does not exist
* @see PreparedStatement#setObject(int, java.lang.Object)
*/
public void setObject(String name, Object value) throws SQLException {
int[] indexes = getIndexes(name);
for (int i = 0; i < indexes.length; i++) {
statement.setObject(indexes[i], value);
}
}
/**
* Sets a parameter.
*
* @param name
* parameter name
* @param value
* parameter value
* @throws SQLException
* if an error occurred
* @throws IllegalArgumentException
* if the parameter does not exist
* @see PreparedStatement#setString(int, java.lang.String)
*/
public void setString(String name, String value) throws SQLException {
int[] indexes = getIndexes(name);
for (int i = 0; i < indexes.length; i++) {
statement.setString(indexes[i], value);
}
}
/**
* Sets a parameter.
*
* @param name
* parameter name
* @param value
* parameter value
* @throws SQLException
* if an error occurred
* @throws IllegalArgumentException
* if the parameter does not exist
* @see PreparedStatement#setInt(int, int)
*/
public void setInt(String name, int value) throws SQLException {
int[] indexes = getIndexes(name);
for (int i = 0; i < indexes.length; i++) {
statement.setInt(indexes[i], value);
}
}
/**
* Sets a parameter.
*
* @param name
* parameter name
* @param value
* parameter value
* @throws SQLException
* if an error occurred
* @throws IllegalArgumentException
* if the parameter does not exist
* @see PreparedStatement#setInt(int, int)
*/
public void setLong(String name, long value) throws SQLException {
int[] indexes = getIndexes(name);
for (int i = 0; i < indexes.length; i++) {
statement.setLong(indexes[i], value);
}
}
/**
* Sets a parameter.
*
* @param name
* parameter name
* @param value
* parameter value
* @throws SQLException
* if an error occurred
* @throws IllegalArgumentException
* if the parameter does not exist
* @see PreparedStatement#setTimestamp(int, java.sql.Timestamp)
*/
public void setTimestamp(String name, Timestamp value) throws SQLException {
int[] indexes = getIndexes(name);
for (int i = 0; i < indexes.length; i++) {
statement.setTimestamp(indexes[i], value);
}
}
/**
* Returns the underlying statement.
*
* @return the statement
*/
public PreparedStatement getStatement() {
return statement;
}
/**
* Executes the statement.
*
* @return true if the first result is a {@link ResultSet}
* @throws SQLException
* if an error occurred
* @see PreparedStatement#execute()
*/
public boolean execute() throws SQLException {
return statement.execute();
}
/**
* Executes the statement, which must be a query.
*
* @return the query results
* @throws SQLException
* if an error occurred
* @see PreparedStatement#executeQuery()
*/
public ResultSet executeQuery() throws SQLException {
return statement.executeQuery();
}
/**
* Executes the statement, which must be an SQL INSERT, UPDATE or DELETE statement; or an SQL statement that returns
* nothing, such as a DDL statement.
*
* @return number of rows affected
* @throws SQLException
* if an error occurred
* @see PreparedStatement#executeUpdate()
*/
public int executeUpdate() throws SQLException {
return statement.executeUpdate();
}
/**
* Closes the statement.
*
* @throws SQLException
* if an error occurred
* @see Statement#close()
*/
public void close() throws SQLException {
statement.close();
}
/**
* Adds the current set of parameters as a batch entry.
*
* @throws SQLException
* if something went wrong
*/
public void addBatch() throws SQLException {
statement.addBatch();
}
/**
* Executes all of the batched statements.
*
* See {@link Statement#executeBatch()} for details.
*
* @return update counts for each statement
* @throws SQLException
* if something went wrong
*/
public int[] executeBatch() throws SQLException {
return statement.executeBatch();
}
}
追加一个stroage文件夹,把这个类放进去。看起来没啥报错
改造下insertByTemplate方法
public UserDTO insertByNamedTemplate(UserDTO dto) {
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
// 改为名称绑定前缀为:
var sql = "insert into [user](account,name,status) values(:account,:name,:status)";
// 使用大神的类
NamedParameterStatement ps = new NamedParameterStatement(conn, sql, new String[] { "id" });
// 设置参数值, 熟悉的感觉来了
ps.setString("account", dto.getAccount());
ps.setString("name", dto.getName());
ps.setInt("status", dto.getStatus());
// 取得转换好的statment
return ps.getStatement();
}
}, keyHolder);
// 不是自增列不能用會異常
dto.setId(keyHolder.getKey().intValue());
return dto;
}
修改UserRepository
运行看看吧
看起来可以, 如果大神的类没有bug,倒是不错的方案了
使用官方的NamedParameterJdbcTemplate实现insert
jdbcTemplte驱动里也有个命名参数绑定的方案,名字有点长。来体验下
修改UserDAO.java
public class UserDAO {
// 数据库对象使用自动注入
@Autowired
private NamedParameterJdbcTemplate namedTemplate;
public UserDTO insertByJdbcNamedTemplate(UserDTO dto) {
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
String sql = "insert into [user](account,name,status) values(:account,:name,:status)";
MapSqlParameterSource sps = new MapSqlParameterSource();
// 指定参数名 包括类型
sps.addValue("account", new SqlParameterValue(Types.VARCHAR, dto.getAccount()));
sps.addValue("name", new SqlParameterValue(Types.NVARCHAR, dto.getName()));
sps.addValue("status", new SqlParameterValue(Types.INTEGER, dto.getStatus()));
namedTemplate.update(sql, sps, keyHolder, new String[] { "id" });
// 不是自增列不能用會異常
dto.setId(keyHolder.getKey().intValue());
return dto;
}
public UserDTO insertByTemplate(UserDTO dto) {
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
// 老方法改造
namedTemplate.getJdbcTemplate().update(new PreparedStatementCreator() {
....
...
这个驱动力既指定了参数名也指定了参数类型,指定类型速度可以快点点吧,就是有点耦合,数据库改了也要改,当然数据库一旦交付用户了基本一般不会再改字段类型,除非有重大失误。到底怎么用就要看实际项目权衡了
修改UserRepository.java
运行下看看
curl http://localhost:8080/user/create -H "Content-Type: application/json" -X POST -d '{
"Id": 0,
"Account": "admin1",
"Name": "管理员1",
"Status": "1"
}'
一次就成功了 ,id也到了20。
数据库保存了
连接Mysql数据库
首先追加驱动
工程右键 Spring > Add Starter
修改数据库 连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/java_demo?characterEncoding=UTF-8
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=xxxx
初始化数据库
CREATE SCHEMA `java_demo`;
use java_demo;
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` nvarchar(45) NOT NULL,
`account` varchar(20) NOT NULL,
`status` int NOT NULL,
PRIMARY KEY (`id`)
)
修改sql
不同数据库写法 可能不同 ,mybatis这里就只要改xml,无需编译了
没啥要改了,运行看看
curl http://localhost:8080/user/create -H "Content-Type: application/json" -X POST -d '{
"Id": 0,
"Account": "admin1",
"Name": "管理员1",
"Status": "1"
}'
返回成功
数据库插入成功
总结
Java驱动选型基本就这样了,NamedParameterJdbcTemplate 这个感觉还是可以的。基本就这个了, Mybatis也是不错的选择,可以彻底分离数据库层。感觉要性能就JdbcTemplate,要效率解耦就Mybatis吧