SpringBoot+JPA+Freemarker 生成动态SQL

「爱情、让人受尽委屈。」 2022-05-11 08:24 485阅读 0赞

在实际开发中,有时候会遇到复杂的统计SQL,这时候Hibernate并不能很好的满足我们的需求,一方面SQL语句写在代码中不美观,可读性比较差,另一方面不方便修改SQL语句。
如果可以像mybaits一样写在XML中的话,就方便许多了。
采用Freemarker模板就可以达到将SQL写在XML文件中的功能。

一、pom引用

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-jpa</artifactId>
  4. <exclusions><!-- 去掉默认配置 -->
  5. <exclusion>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-logging</artifactId>
  8. </exclusion>
  9. </exclusions>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.springframework.boot</groupId>
  13. <artifactId>spring-boot-starter-freemarker</artifactId>
  14. </dependency>
  15. <dependency>
  16. <groupId>org.springframework.boot</groupId>
  17. <artifactId>spring-boot-starter-jdbc</artifactId>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.springframework.boot</groupId>
  21. <artifactId>spring-boot-starter-log4j2</artifactId>
  22. </dependency>

二、写个DTD文件,定义XML文件格式

  1. <!-- HOP Hibernate Dynamic Statement Mapping DTD.
  2. <!DOCTYPE dynamic-hibernate-statement PUBLIC
  3. "-//Haier/HOP Hibernate Dynamic Statement DTD 1.0//EN"
  4. "http://www.wongws.com/dtd/dynamic-hibernate-statement-1.0.dtd">
  5. 这个文件时用来定义动态参数语句,类似itabis
  6. -->
  7. <!--
  8. The document root.
  9. -->
  10. <!ELEMENT dynamic-hibernate-statement (
  11. (hql-query|sql-query)*
  12. )>
  13. <!-- default: none -->
  14. <!-- The query element declares a named Hibernate query string -->
  15. <!ELEMENT hql-query (#PCDATA)>
  16. <!ATTLIST hql-query name CDATA #REQUIRED>
  17. <!-- The sql-query element declares a named SQL query string -->
  18. <!ELEMENT sql-query (#PCDATA)>
  19. <!ATTLIST sql-query name CDATA #REQUIRED>
  20. <!ATTLIST sql-query mode (normal|jdbc|namedJdbc) "normal">

三、DTD解析器

  1. public class DynamicStatementDTDEntityResolver implements EntityResolver {
  2. private static final Logger LOGGER = LoggerFactory.getLogger(DynamicStatementDTDEntityResolver.class);
  3. private static final String HOP_DYNAMIC_STATEMENT = "http://www.wongws.com/dtd/";
  4. public InputSource resolveEntity(String publicId, String systemId) {
  5. InputSource source = null; // returning null triggers default behavior
  6. if (systemId != null) {
  7. LOGGER.debug("trying to resolve system-id [" + systemId + "]");
  8. if (systemId.startsWith(HOP_DYNAMIC_STATEMENT)) {
  9. LOGGER.debug(
  10. "recognized hop dyanmic statement namespace; attempting to resolve on classpath under com/haier/openplatform/dao/hibernate/");
  11. source = resolveOnClassPath(publicId, systemId, HOP_DYNAMIC_STATEMENT);
  12. }
  13. }
  14. return source;
  15. }
  16. private InputSource resolveOnClassPath(String publicId, String systemId, String namespace) {
  17. InputSource source = null;
  18. String path = "dtd/" + systemId.substring(namespace.length());
  19. InputStream dtdStream = resolveInHibernateNamespace(path);
  20. if (dtdStream == null) {
  21. LOGGER.debug("unable to locate [" + systemId + "] on classpath");
  22. if (systemId.substring(namespace.length()).indexOf("2.0") > -1) {
  23. LOGGER.error("Don't use old DTDs, read the Hibernate 3.x Migration Guide!");
  24. }
  25. } else {
  26. LOGGER.debug("located [" + systemId + "] in classpath");
  27. source = new InputSource(dtdStream);
  28. source.setPublicId(publicId);
  29. source.setSystemId(systemId);
  30. }
  31. return source;
  32. }
  33. protected InputStream resolveInHibernateNamespace(String path) {
  34. return this.getClass().getClassLoader().getResourceAsStream(path);
  35. }
  36. }

四、构造存储动态SQL的对象

  1. public class StatementTemplate {
  2. public enum TYPE {
  3. /** hql 查询 */
  4. HQL,
  5. /** sql 查询 */
  6. SQL
  7. }
  8. public StatementTemplate() {
  9. }
  10. public StatementTemplate(TYPE type, Template template) {
  11. this.type = type;
  12. this.template = template;
  13. }
  14. private TYPE type;
  15. private Template template;
  16. public TYPE getType() {
  17. return type;
  18. }
  19. public void setType(TYPE type) {
  20. this.type = type;
  21. }
  22. public Template getTemplate() {
  23. return template;
  24. }
  25. public void setTemplate(Template template) {
  26. this.template = template;
  27. }
  28. }

五、构造动态SQL加载器

  1. public class DefaultDynamicHibernateStatementBuilder implements DynamicHibernateStatementBuilder, ResourceLoaderAware {
  2. private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDynamicHibernateStatementBuilder.class);
  3. private Map<String, String> namedHQLQueries;
  4. private Map<String, String> namedSQLQueries;
  5. private String[] fileNames = new String[0];
  6. private ResourceLoader resourceLoader;
  7. private EntityResolver entityResolver = new DynamicStatementDTDEntityResolver();
  8. /**
  9. * 查询语句名称缓存,不允许重复
  10. */
  11. private Set<String> nameCache = new HashSet<String>();
  12. public void setFileNames(String[] fileNames) {
  13. this.fileNames = fileNames;
  14. }
  15. @Override
  16. public Map<String, String> getNamedHQLQueries() {
  17. return namedHQLQueries;
  18. }
  19. @Override
  20. public Map<String, String> getNamedSQLQueries() {
  21. return namedSQLQueries;
  22. }
  23. @Override
  24. public void init() throws IOException {
  25. namedHQLQueries = new HashMap<String, String>();
  26. namedSQLQueries = new HashMap<String, String>();
  27. boolean flag = this.resourceLoader instanceof ResourcePatternResolver;
  28. for (String file : fileNames) {
  29. if (flag) {
  30. Resource[] resources = ((ResourcePatternResolver) this.resourceLoader).getResources(file);
  31. buildMap(resources);
  32. } else {
  33. Resource resource = resourceLoader.getResource(file);
  34. buildMap(resource);
  35. }
  36. }
  37. // clear name cache
  38. nameCache.clear();
  39. }
  40. @Override
  41. public void setResourceLoader(ResourceLoader resourceLoader) {
  42. this.resourceLoader = resourceLoader;
  43. }
  44. private void buildMap(Resource[] resources) {
  45. if (resources == null) {
  46. return;
  47. }
  48. for (Resource resource : resources) {
  49. buildMap(resource);
  50. }
  51. }
  52. @SuppressWarnings({ "rawtypes" })
  53. private void buildMap(Resource resource) {
  54. InputSource inputSource = null;
  55. try {
  56. inputSource = new InputSource(resource.getInputStream());
  57. XmlDocument metadataXml = readMappingDocument(entityResolver, inputSource,
  58. new OriginImpl("file", resource.getFilename()));
  59. if (isDynamicStatementXml(metadataXml)) {
  60. final Document doc = metadataXml.getDocumentTree();
  61. final Element dynamicHibernateStatement = doc.getRootElement();
  62. Iterator rootChildren = dynamicHibernateStatement.elementIterator();
  63. while (rootChildren.hasNext()) {
  64. final Element element = (Element) rootChildren.next();
  65. final String elementName = element.getName();
  66. if ("sql-query".equals(elementName)) {
  67. putStatementToCacheMap(resource, element, namedSQLQueries);
  68. } else if ("hql-query".equals(elementName)) {
  69. putStatementToCacheMap(resource, element, namedHQLQueries);
  70. }
  71. }
  72. }
  73. } catch (Exception e) {
  74. LOGGER.error(e.toString());
  75. throw new RuntimeException(e);
  76. } finally {
  77. if (inputSource != null && inputSource.getByteStream() != null) {
  78. try {
  79. inputSource.getByteStream().close();
  80. } catch (IOException e) {
  81. LOGGER.error(e.toString());
  82. throw new RuntimeException(e);
  83. }
  84. }
  85. }
  86. }
  87. private void putStatementToCacheMap(Resource resource, final Element element, Map<String, String> statementMap)
  88. throws Exception {
  89. String sqlQueryName = element.attribute("name").getText();
  90. Validate.notEmpty(sqlQueryName);
  91. if (nameCache.contains(sqlQueryName)) {
  92. throw new RuntimeException("重复的sql-query/hql-query语句定义在文件:" + resource.getURI() + "中,必须保证name的唯一.");
  93. }
  94. nameCache.add(sqlQueryName);
  95. String queryText = element.getText();
  96. statementMap.put(sqlQueryName, queryText);
  97. }
  98. private static boolean isDynamicStatementXml(XmlDocument xmlDocument) {
  99. return "dynamic-hibernate-statement".equals(xmlDocument.getDocumentTree().getRootElement().getName());
  100. }
  101. public XmlDocument readMappingDocument(EntityResolver entityResolver, InputSource source, Origin origin) {
  102. return legacyReadMappingDocument(entityResolver, source, origin);
  103. // return readMappingDocument( source, origin );
  104. }
  105. private XmlDocument legacyReadMappingDocument(EntityResolver entityResolver, InputSource source, Origin origin) {
  106. // IMPL NOTE : this is the legacy logic as pulled from the old
  107. // AnnotationConfiguration code
  108. Exception failure;
  109. ErrorLogger errorHandler = new ErrorLogger();
  110. SAXReader saxReader = new SAXReader();
  111. saxReader.setEntityResolver(entityResolver);
  112. saxReader.setErrorHandler(errorHandler);
  113. saxReader.setMergeAdjacentText(true);
  114. saxReader.setValidation(true);
  115. Document document = null;
  116. try {
  117. // first try with orm 2.1 xsd validation
  118. setValidationFor(saxReader, "orm_2_1.xsd");
  119. document = saxReader.read(source);
  120. if (errorHandler.hasErrors()) {
  121. throw errorHandler.getErrors().get(0);
  122. }
  123. return new XmlDocumentImpl(document, origin.getType(), origin.getName());
  124. } catch (Exception e) {
  125. LOGGER.debug("Problem parsing XML using orm 2.1 xsd, trying 2.0 xsd : {}", e.getMessage());
  126. failure = e;
  127. errorHandler.reset();
  128. if (document != null) {
  129. // next try with orm 2.0 xsd validation
  130. try {
  131. setValidationFor(saxReader, "orm_2_0.xsd");
  132. document = saxReader.read(new StringReader(document.asXML()));
  133. if (errorHandler.hasErrors()) {
  134. errorHandler.logErrors();
  135. throw errorHandler.getErrors().get(0);
  136. }
  137. return new XmlDocumentImpl(document, origin.getType(), origin.getName());
  138. } catch (Exception e2) {
  139. LOGGER.debug("Problem parsing XML using orm 2.0 xsd, trying 1.0 xsd : {}", e2.getMessage());
  140. errorHandler.reset();
  141. if (document != null) {
  142. // next try with orm 1.0 xsd validation
  143. try {
  144. setValidationFor(saxReader, "orm_1_0.xsd");
  145. document = saxReader.read(new StringReader(document.asXML()));
  146. if (errorHandler.hasErrors()) {
  147. errorHandler.logErrors();
  148. throw errorHandler.getErrors().get(0);
  149. }
  150. return new XmlDocumentImpl(document, origin.getType(), origin.getName());
  151. } catch (Exception e3) {
  152. LOGGER.debug("Problem parsing XML using orm 1.0 xsd : {}", e3.getMessage());
  153. }
  154. }
  155. }
  156. }
  157. }
  158. throw new InvalidMappingException("Unable to read XML", origin.getType(), origin.getName(), failure);
  159. }
  160. private void setValidationFor(SAXReader saxReader, String xsd) {
  161. try {
  162. saxReader.setFeature("http://apache.org/xml/features/validation/schema", true);
  163. // saxReader.setFeature( "http://apache.org/xml/features/validation/dynamic",
  164. // true );
  165. if ("orm_2_1.xsd".equals(xsd)) {
  166. saxReader.setProperty("http://apache.org/xml/properties/schema/external-schemaLocation",
  167. "http://xmlns.jcp.org/xml/ns/persistence/orm " + xsd);
  168. } else {
  169. saxReader.setProperty("http://apache.org/xml/properties/schema/external-schemaLocation",
  170. "http://java.sun.com/xml/ns/persistence/orm " + xsd);
  171. }
  172. } catch (SAXException e) {
  173. saxReader.setValidation(false);
  174. }
  175. }
  176. }
  177. public interface DynamicHibernateStatementBuilder {
  178. /**
  179. * hql语句map
  180. *
  181. * @return
  182. */
  183. Map<String, String> getNamedHQLQueries();
  184. /**
  185. * sql语句map
  186. *
  187. * @return
  188. */
  189. Map<String, String> getNamedSQLQueries();
  190. /**
  191. * 初始化
  192. *
  193. * @throws IOException
  194. */
  195. void init() throws IOException;
  196. void setFileNames(String[] fileNames);
  197. void setResourceLoader(ResourceLoader resourceLoader);
  198. }

六、利用Freemarker模板书写动态SQL

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <!DOCTYPE dynamic-hibernate-statement PUBLIC "-//Haier/HOP Hibernate Dynamic Statement DTD 1.0//EN"
  3. "http://www.wongws.com/dtd/dynamic-hibernate-statement-1.0.dtd">
  4. <dynamic-hibernate-statement>
  5. <sql-query name="resource.getUser" mode="normal">
  6. <![CDATA[
  7. select * from t_user t where t.user_id=${userid}
  8. <#if password??>
  9. and t.password=${password}
  10. </#if>
  11. ]]>
  12. </sql-query>
  13. </dynamic-hibernate-statement>

以上就是功能实现的关键代码,但是利用Freemarker有个问题,就是没法解决SQL注入的问题,这里我的操作就是可以使用Freemarker生成动态SQL,然后再利用JdbcTemplate或NamedParameterJdbcTemplate来执行SQL。如将上面的SQL改写成这样:

  1. <sql-query name="resource.getUser" mode="normal">
  2. <![CDATA[
  3. select * from t_user t where t.user_id=:userid
  4. <#if password??>
  5. and t.password=:password
  6. </#if>
  7. ]]>
  8. </sql-query>

这样的话就可以绕过Freemarker的占位符使用,然后再通过NamedParameterJdbcTemplate执行SQL。
当然实际SQL不可能这么简单,肯定会遇到Map或者List等问题,这些只要网上查询Freemarker的语法,就能迎刃而解。
下面奉上源码,由于这只是个Demo,代码实在是粗制滥造了点,但是功能还是能够实现的,在com.example.demo下的DemoApplicationTests类上,运行Junit Test。当然,要是实现mysql访问,还得自己配数据库连接。

https://download.csdn.net/download/dashuaigege642/10681049

参考:

https://blog.csdn.net/crazycoder2010/article/details/7414152

发表评论

表情:
评论列表 (有 0 条评论,485人围观)

还没有评论,来说两句吧...

相关阅读