0

I am trying to isolate JPQL queries in Spring Boot.

I am in a SpringBoot project. There are many ways to query from @Query(...) on the entity itself, @Query(...) on the repository method (with HQL(query hibernate POO) or JPQL(query native POO) . Also with the properties (file.properties with the queries) + method name...

I am looking for this way: I've been reading: Similar to how Java EE does it. https://www.baeldung.com/jpa-query-parameters

I put a simple example, so that it can be appreciated.

Note: This class does not extend from "JPA". It's a regular DAO with @Repository as used in JEE.

  @Override
    public List<User> findAllUsersByRole(String roleName, Long roleID) { 
        TypedQuery<User> query = entityManager.createQuery(
                " SELECT u FROM User u " +
                " RIGHT JOIN UserRoles ur ON ur.userId = u.id " +
                " RIGHT JOIN Role r ON r.id = ur.roleId " +
                " WHERE r.name like :roleName AND r.id =:roleID ", User.class);
        //query.setParameter(1, roleName).getSingleResult();
        //query.setParameter(1, roleID).getSingleResult();
        query.setParameter("roleName", roleName);
        query.setParameter("roleID", roleID);
        List<User> user = query.getResultList();
        return user;
    }

Java quoting is pretty annoying.

The problem is that when there are many tables and many lines this is quite cumbersome (with the single quotes ""+..., I miss the JS interpolation in these cases), but sometimes, you need something native in "JPQL" to fetch different tables with certain aspects.

I also read this to supplement the separation: Clean way to externalize long (+20 lines sql) when using spring jdbc?

I have read that I can do it with:

<util:properties id="sqls" location="classpath:oracle/sqls.xml" />

.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>Employee Queries</comment>
    <string key="employee.insert">
         INSERT INTO......
</string>
</properties>

.

@Autowired
@Qualifier("sqls")
private Properties sqls;
String sql = sqls.getProperty("employee.insert");

I have the problem with "<util:properties id="sqls" location="classpath:oracle/sqls.xml" />". Assuming that I "can't" create an XML, how can I add that to the Spring Boot context and have it detect it for me?

My question is, how can I separate the JPQL code in SpringBoot into an xml file, just like this:

--------XML

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>User Queries</comment>
    <entry key="user.get">
        SELECT ....
    </entry>
</properties>

------Java DAO (custom in Spring Boot)

Note: This class does not extend from "JPA". It's a regular DAO with @Repository as used in JEE.

...
  @Autowired
    @Qualifier("sqls")
    private Properties sqls;
    String sql = sqls.getProperty("user.get");

    @Override
    public List<User> findAllUsersByRole(String roleName, Long roleID) {
        TypedQuery<User> query = entityManager.createQuery(sql)

        query.setParameter("roleName", roleName);
        query.setParameter("roleID", roleID);
        List<User> user = query.getResultList();
        return user;
    }
 ...

I've read this, but I'm still not clear. https://docs.spring.io/spring-boot/docs/1.0.1.RELEASE/reference/html/boot-features-external-config.html

  • I know how to do it in Spring/Springboot with:
    • JPA method naming conventions for queries
    • JPA method naming conventions for queries + properties

But with the entityManager I am not able to isolate the query to another file. Therefore, this section would help me a lot.

I know that there are other ways, but I am only looking for the one that I comment on in the post.

General Grievance
  • 4,555
  • 31
  • 31
  • 45
  • Now if only JPA had something for that... O wait they have an `orm.xml`. Or use a newer java version and just use Java text-blocks no String concat needed. – M. Deinum Oct 10 '22 at 06:40
  • Right an orm.xml a hibernate mapping. The problem is that in this part I am doing it the old fashioned way. I simply need to add this "". to the configuration class @Configuration, so that it detects it for me. But I do not know how to do it. – Naslo Flaquez Oct 10 '22 at 11:37
  • `orm.xml` isn't a hibernate mapping it is a JPA default. The way you are working now is simply not going to work. Instead use the proper tools instead of working around them and reinventing a crappy wheel. But if you really want, just load the properties, the XML is nothing more than loading the properties using a `PropertiesLoader` from Spring. However I strongly suggest the default path. – M. Deinum Oct 10 '22 at 11:39
  • In Spring/SpringBoot I can't find a way so that the @Configuration configuration can add an XML property (no SAX problems) and only have it added once to the config. I may be doing it wrong. There may be some example on the internet. If you could provide information on any example or web page I would be grateful. – Naslo Flaquez Oct 10 '22 at 14:28
  • I give up. Because you are just doing it wrong (reinventing a crappy wheel) and I already gave you the replacement for the XML configuration (the `PropertiesLoader`). Nor will it work and fail miserably with a `NullPointerException`. – M. Deinum Oct 10 '22 at 14:30
  • " I strongly suggest the default path" The "default" path. For a mapped orm.xml file it is like this: https://i.ibb.co/4KFHdcr/1.png in interfaces (extends: repository or JPARepository, Crud repo ...) + Obj methodName (params...) I don't want it like that (by default), because I have to fill a DTO. You will tell me, it is simple, if you have to fill a DTO, then in the output of the SELECT (you put the fields you need for that DTO). The question is what if I have 2 DTOs that do 2 different things? – Naslo Flaquez Oct 10 '22 at 14:31
  • So I need a Custom and return or add (to the necessary DTOs/Objects/Generic... or do whatever) whatever I want from the query. Therefore, I need a custom DAO and that method does what I ask without being tied to anything. – Naslo Flaquez Oct 10 '22 at 14:31
  • That is not the default that is partially the default (orm.xml) and using Spring Data JPA... **Nothing** is preventing you from **not** using Spring Data JPA and just use the `createNamedQuery` and provide your own result. **That** is the default way of doing it with JPA. – M. Deinum Oct 10 '22 at 14:44

2 Answers2

0

Instead of reinventing a crappy wheel by providing an XML based properties file. Use the proper techniques and write an orm.xml (which is a JPA standard) and write the named query in there.

<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm
                                     http://xmlns.jcp.org/xml/ns/persistence/orm_2_0.xsd" version="2.1">

    <!-- JPA Named Native Queries -->
    <named-native-query name="User.findAllUsersByRole">
        <query>SELECT u FROM User u
               RIGHT JOIN UserRoles ur ON ur.userId = u.id
               RIGHT JOIN Role r ON r.id = ur.roleId
               WHERE r.name like :roleName AND r.id =:roleID
        </query>
    </named-native-query>
</entity-mappings>

Then in your Java code just reference this named query

@Override
public List<User> findAllUsersByRole(String roleName, Long roleID) {
   TypedQuery<User> query = entityManager.createNamedQuery("User. findAllUsersByRole", User.class);
   query.setParameter("roleName", roleName);
   query.setParameter("roleID", roleID);
   return query.getResultList();
}

No need to bolt something additional on top with properties files, additional XML files etc. Just use the plain JPA standards and provide the queries in the orm.xml.

If the only thing you are trying to do is to remove the string concat upgrade to a newer Java version and just use text blocks. No need for concat just a large block of text (just as in JavaScript).

@Override
public List<User> findAllUsersByRole(String roleName, Long roleID) { 
    TypedQuery<User> query = entityManager.createQuery("""
               SELECT u FROM User u
                RIGHT JOIN UserRoles ur ON ur.userId = u.id
                RIGHT JOIN Role r ON r.id = ur.roleId
                WHERE r.name like :roleName AND r.id =:roleID""", User.class);        
    query.setParameter("roleName", roleName);
    query.setParameter("roleID", roleID);
    return query.getResultList();
}
M. Deinum
  • 115,695
  • 22
  • 220
  • 224
  • Interesting, I didn't know about this way """ query """. But I have to separate the code from the queries depending on whether it is an orm.xml or a __.properties. With ateNamedQuery("User.findAllUsersByRole", User.class); I get an error "Named query exists but its result type is not compatible" I have tried to change the version of the mapping XML to version 2.1 but still the same. I'm on it. – Naslo Flaquez Oct 10 '22 at 15:01
  • I'm getting different errors about doing it this way. I'll try to fix them and let you know. Thanks in advance. – Naslo Flaquez Oct 10 '22 at 15:37
  • Many thanks. I corrected errors. I'll keep the standard by calling it "orm.xml". I will not add other orm.xml to the @Configuration. I was going to have a stack of orms.xml documents to make it neater. But thanks anyway. – Naslo Flaquez Oct 10 '22 at 15:53
  • You can have multiple `orm.xml` files but you would have to provide the list of names to the `EntityManagerFactory`. If you are using Spring I do believe you can pass a pattern (something like `classpath:/orm-*.xml` and it will detect and load all of them (but I'm not 100% sure). Regarding the mappings for the result I always forget if I do or don't need an explicit result-set mapping or not. What you can do is use the non-typed query and do the mapping yourself (you will get a list of object[]). If you `unwrap` to a `Session` (plain Hibernate) you can do more with regards to mapping a result. – M. Deinum Oct 11 '22 at 05:14
0

In the end I was able to incorporate multiple ORMs which is the important thing. I didn't do it with properties, because it simply gave too many errors (SAXParse errors, file, persistence...). I did it with a @Configuration class starting from this example:

Spring Data JPA having multiple orm.xml files

I got something like this:

 entityManagerFactoryBean.setMappingResources("META-INF/orms/custom-orm-1.xml", "META-INF/orms/custom-orm-2.xml","META-INF/orm.xml");

At this point, the JPQL will behave strangely (I suppose because it will be missing some type of configuration property that I am overlooking) and the fields/columns that have camel case will have to be pondered @Column(name = "mysql_column_name"), such as

firstName = does not detect it as first_name, it detects it as firstName.

In this section you will have to put so that it detects it correctly

@Column(name = "first_name")
private String firstName;

and the same for the other fields/columns that carry camelCase. Same for entities.

I guess there is some property or properties that does that "firstName -> first_name" conversion for you. But for me, it's not a problem. If I find it, I'll post it. However, it works perfectly with the @Column and @Table with their respective camelcases and I can already have what I was looking for for this post.

Thanks to M. Deinum for the help.

-------------------- EDITED ----------------------

After reading a lot I found the solution why. It seems that if you declare LocalContainerEntityManagerFactoryBean, since there is no @Autowired for this case to be able to add the properties one by one to the other ones it had. So you have to add it back, both the dialect and the strategy. Looking at a lot of information I came to the same conclusion as our partner @genki98

EntityManager doesn't translate camel case to snake case

@Bean
public LocalContainerEntityManagerFactoryBean businessEntityManagerFactory(DataSource businessDataSource) {
    LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
    emf.setDataSource(businessDataSource);
    emf.setPackagesToScan("com.xxx.yyy");

    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    vendorAdapter.setShowSql(Boolean.valueOf(env.getProperty("spring.jpa.show-sql")));
    emf.setJpaVendorAdapter(vendorAdapter);

    HashMap<String, Object> properties = new HashMap<>();
    properties.put("hibernate.dialect", env.getProperty("spring.jpa.properties.hibernate.dialect"));
    properties.put("hibernate.format_sql", env.getProperty("spring.jpa.properties.hibernate.format_sql"));
    properties.put("hibernate.hbm2ddl.auto", env.getProperty("spring.jpa.properties.hibernate.hbm2ddl.auto"));
    properties.put("hibernate.physical_naming_strategy", "org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy");
    properties.put("hibernate.implicit_naming_strategy", "org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy");
    emf.setJpaPropertyMap(properties);
    // Multiples ORMS mapping
    emf.setMappingResources("META-INF/orms/custom-orm-1.xml", "META-INF/orms/custom-orm-2.xml","META-INF/orm.xml");
    return emf;
}

If you add

Properties properties = new Properties();
....
entityManagerFactoryBean.setJpaProperties(properties);

You get nulls you have to do it with HashMap. It looks like the entityManagerFactoryBean.setJpaProperties(properties); does not merge correctly: "ollectionUtils.mergePropertiesIntoMap(jpaProperties, this.jpaPropertyMap);"

So the Hibernate dialect for ORM queries works correctly without having to put the

@Column(name = "first_name")
private String firstName;

I hope this post helps as much as it helped me.