Accessing a private super class field in MyBatis - java

Consider the following POJOs that will be used for holding the parameters that will be passed to a query.
public class RegionKey {
private BigDecimal rgnId;
private Country country;
//setters and getters.
}
public class Country {
private BigDecimal cntryId;
//setters and getters.
}
public class Region extends RegionKey {
private String rgnNm;
private String desc;
//setters and getters
}
public class Customer {
private BigDecimal custId;
private Region rgn;
}
Consider the CustomerMapper interface for MyBatis
public interface CustomerMapper {
int deleteByPrimaryKey(#Param("custRecord") Customer key);
}
Consider a snippet from the CustomerMapper.xml file (QUery 1)
<delete id="deleteByPrimaryKey">
delete from CUSTOMER
where CUST_ID = #{custRecord.custId,jdbcType=DECIMAL}
and RGN_ID =
cast(#{custRecord.rgn.rgnId,jdbcType=CHAR} as char(10))
</delete>
The above query works perfectly fine. Modifying the above query with the following if-test works fine as well (Query 2)
<delete id="deleteByPrimaryKey">
delete from CUSTOMER
where CUST_ID = #{custRecord.custId,jdbcType=DECIMAL}
<if test="custRecord.rgn.rgnId != null">
and RGN_ID = cast(#{custRecord.rgn.rgnId,jdbcType=CHAR} as
char(10))
</if>
</delete>
Modifying the query as follows causes a runtime exception (Query 3)
<delete id="deleteByPrimaryKey">
delete from CUSTOMER
where CUST_ID = #{custRecord.custId,jdbcType=DECIMAL}
<if test="custRecord.rgn.country.cntryId != null">
and CNTRY_ID =
cast(#{custRecord.rgn.country.cntryId,jdbcType=CHAR} as
char(10))
</if>
</delete>
I get a org.apache.ibatis.ognl.NoSuchPropertyException at runtime for query number 3. I am unable to understand why this happens. If I can access the rgnId field from custRecord.rgn in query 2, I should technically be able to access the cntryId field from custRecord.rgn.country in query number 3.

MyBatis expects (as most frameworks do) that "properties" follow the Java Bean specs, so that a property foo is mapped to a getter getFoo() and (optionally) a setter setFoo() (the name of the private field can be different -it can even not exist!- but very often it has same as the property).
So, in you example you should have
public class RegionKey {
private Country country;
...
public Country getCountry() {
...
}
}
and so on. Java IDEs (eg Eclipse) understand this convention, and allow you to generate these getters/setters for you, so you don't have to type them.

Related

spring data #query and projection should select only needed fields but this is not a case

Supposing I have an entity (simplified)
#Entity
FooEntity {
long id;
String name;
int age;
boolean deleted;
}
And I want to use spring projections to select only id and name fields of this entity
So I created first interface and class that implements it
public interface DictionaryInterface {
Long getId();
String getName();
}
#Value
class DictionaryObject implements DictionaryInterface {
Long id;
String name;
}
then I created repository interface with method that makes use of projection interface
<T> List<T> findByDeleted(boolean deleted, <Class<T> type);
and tried two calls and looked at generated sql
findByProjection(false, DictionaryInterface.class)
// here sql select includes all fields even those that were not needed (age field)
findByProjection(false, DictionaryObject.class) ->
// here sql select includes only id and name fields
my understanding from reading spring data docs is that behavior should be identical and I must select only needed fields but i clearly see that it is not working
where am I wrong ?

Hibernate fetching deleted java enum types from db

I have an Enum class which has some values.
We've decided to remove one of these values and its all implementation from the code.
We dont want to delete any records from DB.
My Enum class is something like this:
public enum CourseType {
VIDEO("CourseType.VIDEO"),
PDF("CourseType.PDF"),
QUIZ("CourseType.QUIZ"),
SURVEY("CourseType.SURVEY"),
POWERPOINT("CourseType.POWERPOINT") //*this one will be removed*
...
}
My Course Entity:
#Entity
#Table(name = "CRS")
public class Course {
#Column(name = "COURSE_TYPE")
#Enumerated(EnumType.STRING)
private CourseType courseType;
#Column(name = "AUTHOR")
private String author;
....
#Override
public CourseType getCourseType() {
return courseType;
}
#Override
public void setCourseType(CourseType courseType) {
this.courseType = courseType;
}
....
}
After I removed the Powerpoint type from the Java Class and tried to fetch some values from the DB,
I get a mapping error for the removed type.
I have a code like this:
Course course = courseService.get(id);
If I gave a course id which its type is 'POWERPOINT' in the database,
the method gets the following error:
java.lang.IllegalArgumentException: Unknown name value [POWERPOINT]
for enum class [com.tst.enums.CourseType] at
org.hibernate.type.EnumType$NamedEnumValueMapper.fromName(EnumType.java:461)
at
org.hibernate.type.EnumType$NamedEnumValueMapper.getValue(EnumType.java:449)
at org.hibernate.type.EnumType.nullSafeGet(EnumType.java:107) at
org.hibernate.type.CustomType.nullSafeGet(CustomType.java:127) at
org.hibernate.type.AbstractType.hydrate(AbstractType.java:106) at
org.hibernate.persister.entity.AbstractEntityPersister.hydrate(AbstractEntityPersister.java:2912)
at org.hibernate.loader.Loader.loadFromResultSet(Loader.java:1673)
Is there any way when I try to retrieve a query result from DB,
hibernate will not fetch if that records' course_type column doesn't match with the any of the enum values in the code?
Do I have to use some kind of filter?
You can try use annotation #filter
#Filter(name = "myFilter", condition = "courseType <> 'CourseType.POWERPOINT'")
and enable it
session.enableFilter("myFilter")
If you can't use filters,
something like the following should work:
Add POWERPOINT back into the enum.
Add a deleted flag to the POWERPOINT enum value.
After the course list is loaded, remove courses that have a deleted courseType value.
New CourseType enum:
public enum CourseType
{
VIDEO("CourseType.VIDEO", false),
POWERPOINT("CourseType.POWERPOINT", true);
private boolean deletedFlag;
public CourseType(
existingParameter, // do whatever you are currently doing with this parameter
deletedFlagValue)
{
// code to handle existing parameter
deletedFlag = deletedFlagValue;
}

MyBatis, Select Provider and SQLBuilder

this is more than a simple question and my English is not as good as I want... I'll try my best.
I use java 8, with Mybatis 3.4.6 over Postgres 9.6 and I need to do a custom dynamic query.
In my mapper.java class I've created a method to use with myBatis SQL Builder class
#SelectProvider(type = PreIngestManager.class, method = "selectPreIngestsSQLBuilder")
#Results({ #Result(property = "id", column = "id"), #Result(property = "inputPath", column = "input_path"),
#Result(property = "idCategoriaDocumentale", column = "id_categoria_documentale"), #Result(property = "idCliente", column = "id_cliente"),
#Result(property = "outputSipPath", column = "output_sip_path"), #Result(property = "esito", column = "esito"),
#Result(property = "stato", column = "stato"), #Result(property = "pathRdp", column = "path_rdp"),
#Result(property = "dataInizio", column = "data_inizio"), #Result(property = "dataFine", column = "data_fine") })
List<PreIngest> selectPreIngestsByFilters(#Param("idCatDoc") Long idCatDoc, #Param("nomePacchetto") String nomePacchetto,
#Param("dataInizioInferiore") Date dataInizioInferiore, #Param("dataInizioSuperiore") Date dataInizioSuperiore,
#Param("statiPreIngest") String statiPreIngest);
I have specified the #SelectProvider annotation, class and method to point at, which, in the example is PreIngestManager.class and selectPreIngestsSQLBuilder method.
This is the method
public String selectPreIngestsSQLBuilder(Map<String, Object> params) {
return new SQL() {
{
SELECT("*");
FROM("pre_ingest");
WHERE("id_categoria_documentale = #{idCatDoc}");
if (params.get("nomePacchetto") != null)
WHERE("input_path like '%' || #{nomePacchetto}");
if (params.get("dataInizioInferiore") != null) {
if (params.get("dataInizioSuperiore") != null) {
WHERE("data_inizio between #{dataInizioInferiore} and #{dataInizioSuperiore}");
} else {
WHERE("data_inizio >= #{dataInizioInferiore}");
}
} else {
if (params.get("dataInizioSuperiore") != null) {
WHERE("data_inizio <= #{dataInizioSuperiore}");
}
}
if (params.get("statiPreIngest") != null)
WHERE("stato in (#{statiPreIngest})");
ORDER_BY("id ASC");
}
}.toString();
}
and these are my questions:
have I to specify #Results annotation and every #Result , or can I use a java model class ? I have tried with #ResultMap(value = { "mycompany.model.PreIngest" }) but it did not work.
Most of all, as stated on documentation, with SQL builder you can access method parameters having them as final objects
// With conditionals (note the final parameters, required for the anonymous inner class to access them)
public String selectPersonLike(final String id, final String firstName,
final String lastName) {
return new SQL() {{
SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
FROM("PERSON P");
if (id != null) {
WHERE("P.ID like #{id}");
}
if (firstName != null) {
WHERE("P.FIRST_NAME like #{firstName}");
}
if (lastName != null) {
WHERE("P.LAST_NAME like #{lastName}");
}
ORDER_BY("P.LAST_NAME");
}}.toString();
}
But if I put those final in my method I can't access them. Do I need to delete the #Param from the method declaration? Do SQLBuilder need to be called without #SelectProvider ? Am I mixing solutions ?
As far as I have researched, for now I see 3 methods to do a dynamic query, or a custom where condition.
To use MyBatisGenerator library and combine where condition as search criteria to use with SelectByExample method. (I use this when the query is simple)
To Write an SQL query directly, modifying XML mapper files using if, choose, statements and others as descripted here
To use SQL Builder class with #SelectProvider annotation.
Do you know when prefer the 2° method over the 3° one ? Why in the 3° method documentation I can't find how to use it ? There is written how to create custom queries but not how to launch them.
Thank a lot for your time and your suggestions.
I don't know whether you already found the answer, I just want to share my experience. Btw please forgive my english if it wasn't good.
Note: I use MyBatis 3.4.6 and Spring Framework.
have I to specify #Results annotation and every #Result , or can I use a java model class ?
Actually you can do either one.
if you want to use #Results and #ResultMap, you just need to specify #Results annotation just once in one mapper file. The trick is you need to specify id for the Results to be used in other functions.
Using truncated version of your classes, eg:
#Results(id="myResult", value= {
#Result(property = "id", column = "id"),
#Result(property = "inputPath", column = "input_path"),
#Result(property = "idCategoriaDocumentale", ... })
List<PreIngest> selectPreIngestsByFilters(#Param("idCatDoc") Long idCatDoc, #Param("nomePacchetto") String nomePacchetto, ...);
Then in another function you can use #ResultMap with the value refer to id from #Results mentioned before.
#ResultMap("myResult")
List<PreIngest> selectPreIngestsBySomethingElse(....);
..., or can I use a java model class ?
You can use java model class as result without using #Results and #ResultMap, but you have to make sure your java model class has the same properties/fields as the result of your query. Database tables usually have fields with snake_case. Since java is using camelCase, you have to add settings to your mybatis-config.xml file.
This is what I usually add to the mybatis-config.xml
<?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>
<settings>
<!-- changes from the defaults -->
<setting name="lazyLoadingEnabled" value="false" />
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="jdbcTypeForNull" value="NULL"/>
</settings>
</configuration>
The important one is mapUnderscoreToCamelCase, set this to true than you can use your java model class without the hassle of #Results and #ResultMap. You can find all the explanation of the settings in MyBatis 3 Configuration.
This is the example using your classes,
The class:
public class PreIngest {
private Long idCategoriaDocumentale;
private Long idCliente;
........ other fields
........ setter, getter, etc
}
The mapper file:
List<PreIngest> selectPreIngestsByFilters(#Param("idCatDoc") Long idCatDoc, #Param("nomePacchetto") String nomePacchetto, ...);
Now onwards to SqlBuilder.
But if I put those final in my method I can't access them. Do I need to delete the #Param from the method declaration? Do SQLBuilder need to be called without #SelectProvider ?
I can't answer about those final in your method since I never made SqlBuilder class with final parameters.
For SqlBuilder you must use #SelectProvider, #InsertProvider, #UpdateProvider or #DeleteProvider and it depends on the query you use.
In my experience with SQLBuilder, #Param is necessary if you need more than one parameters and use Map params to access it from the SqlBuilder class. If you don't want to use #Param in the mapper file, then you need to make sure there is only one parameter in the said mapper function. You can use java model class as the parameter though if you just specify one parameter.
If using your class for example, you can have one class
public class PersonFilter {
private Long id;
private String firstName;
private String lastName;
...... setter, getter, etc
}
the mapper function
#SelectProvider(type=PersonSqlBuilder.class, method="selectPersonLike")
List<Person> selectPersonLike(PersonFilter filter);
the SqlBuilder class
public class PersonSqlBuilder {
public String selectPersonLike(PersonFilter filter) {
return new SQL() {{
SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
FROM("PERSON P");
if (filter.getId() != null) {
WHERE("P.ID like #{id}");
}
if (filter.getFirstName() != null) {
WHERE("P.FIRST_NAME like #{firstName}");
}
if (filter.getLastName() != null) {
WHERE("P.LAST_NAME like #{lastName}");
}
ORDER_BY("P.LAST_NAME");
}}.toString();
}
}
That's it. Hopefully my experience can help.
I don't know how to do this with the sql builder but I do have an idea how to do this with an xml mapper file:
<?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">
<mapper namespace="path.to.class.PreIngestMapper">
<resultMap id="preIngestManager" type="path.to.class.PreIngestManager">
<id property="id" column="id" />
<result property="id" column="id" />
<result property="inputPath" column="input_path" />
<result property="idCategoriaDocumentale" column="id_categoria_documentale" />
...
</resultMap>
<select id="selectPreIngests" parameterType="Map" resultMap="preIngestManager">
SELECT *
FROM pre_ingest
WHERE id_categoria_documentale = #{idCatDoc}
<if test = "nomePacchetto != null">
and input_path like '%' || #{nomePacchetto}
</if>
...
;
</select>
</mapper>

How to get Column names of JPA entity

All my JPA entity classes implement an interface called Entity which is defined like this:
public interface Entity extends Serializable {
// some methods
}
Some of the fields of my JPA entity have #Column annotation on top of them and some don't. MyEntity class is defined like below:
#Entity
public class MyEntity implements Entity {
#Id
private Long id; // Assume that it is auto-generated using a sequence.
#Column(name="field1")
private String field1;
private SecureString field2; //SecureString is a custom class
//getters and setters
}
My delete method accepts an Entity.
#Override
public void delete(Entity baseEntity) {
em.remove(baseEntity); //em is entityManager
}
Whenever the delete method is invoked I want three things inside my delete method:
1) Fields of MyEntity that are of type SecureString
2) Column name of that particular field in DB (The field may or may not have #Column annotation)
3) The value of id field
Note that when the delete() method is invoked, we don't know for which entity it is invoked, it may be for MyEntity1, MyEntity2 etc.
I have tried doing something like below:
for (Field field : baseEntity.getClass().getFields()) {
if (SecureString.class.isAssignableFrom(field.getType())) {
// But the field doesn't have annotation #Column specified
Column column = field.getAnnotation(Column.class);
String columnName = column.name();
}
}
But this will only work if the field has #Column annotation. Also it doesn't get me other two things that I need. Any ideas?
Hibernate can use different naming strategies to map property names, which are defined implicitly (without #Column(name = "...")). To have a 'physical' names you need to dive into Hibernate internals. First, you have to wire an EntityManagerFactory to your service.
#Autowired
private EntityManagerFactory entityManagerFactory;
Second, you have to retrieve an AbstractEntityPersister for your class
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
AbstractEntityPersister persister = ((AbstractEntityPersister)sessionFactory.getClassMetadata(baseEntity.getClass()));
Third, you're almost there with your code. You just have to handle both cases - with and without #Column annotation. Try this:
for (Field field : baseEntity.getClass().getFields()) {
if (SecureString.class.isAssignableFrom(field.getType())) {
String columnName;
if (field.isAnnotationPresent(Column.class)) {
columnName = field.getAnnotation(Column.class).name();
} else {
String[] columnNames = persister.getPropertyColumnNames(field.getName());
if (columnNames.length > 0) {
columnName = columnNames[0];
}
}
}
}
Note that getPropertyColumnNames() retrieves only 'property' fields, that are not a part of primary key. To retrieve key column names, use getKeyColumnNames().
And about id field. Do you really need to have all #Id's in child classes? Maybe would better to move #Id to Entity class and mark this class with #MappedSuperclass annotation? Then you can retrieve it just with baseEntity.getId();

JPA POJO of joined table in Java

What I would like to realize is the following:
I have a dashboard class and a user class.
In my (Java EE project) Java code I would like to get all dashboards, to which the user has been subscribed.
The database contains a table (dashboard_users), with the following fields: idUser, idDashboard, isDefault en ID.
There should also be a Java POJO of the joined tabled.
My question:
How should the JPA M-M connection between these three classes look like (Dashboard.java/User.java/UserDashboard.java)?
I followed a lot of tutorials and examples, but for some reason there are always errors or other problems. It would be very welcome if someone could give an example, so I can see what I am doing wrong.
Thank you
Given the extra attribute on the association table you are going to need to model it (via a UserDashboard.java class as you asked). Quite unfortunate, as it adds a significant amount of work to your model layer.
If you find you do not need the extra attribute after all then I would model User with a set of Dashboards, linked directly via a #JoinTable.
One way you could do this would be to see the relationship between User and Dashboard as a map in which the Dashboard is a key, there being an entry for every Dashboard associated with the User, and the value is a flag indicating whether that Dashboard is the default for that User. I admit this is a bit forced; it's an odd way to look at the relationship, perhaps even suspect as has been charged.
But the advantage of this view is that it lets you map the living daylights out of everything like this:
#Entity
public class Dashboard {
#Id
private int id;
private String name;
public Dashboard(int id, String name) {
this.id = id;
this.name = name;
}
protected Dashboard() {}
}
#Entity
public class User {
#Id
private int id;
private String name;
#ElementCollection
private Map<Dashboard, Boolean> dashboards;
public User(int id, String name) {
this.id = id;
this.name = name;
this.dashboards = new HashMap<Dashboard, Boolean>();
}
protected User() {}
// disclaimer: the following 'business logic' is not necessarily of the finest quality
public Set<Dashboard> getDashboards() {
return dashboards.keySet();
}
public Dashboard getDefaultDashboard() {
for (Entry<Dashboard, Boolean> dashboard : dashboards.entrySet()) {
if (dashboard.getValue()) {
return dashboard.getKey();
}
}
return null;
}
public void addDashboard(Dashboard dashboard) {
dashboards.put(dashboard, false);
}
public void setDefaultDashboard(Dashboard newDefaultDashboard) {
Dashboard oldDefaultDashboard = getDefaultDashboard();
if (oldDefaultDashboard != null) {
dashboards.put(oldDefaultDashboard, false);
}
dashboards.put(newDefaultDashboard, true);
}
}
This maps a table structure which looks like this Hibernate-generated SQL, which i think is roughly what you want. The generated names on the User_dashboards table are pretty shoddy; you could customise them quite easily with some annotations or some XML. Personally, i like to keep all the filthy details of the actual mapping between the objects and the database in an orm.xml; here's what you'd need to add to use more sensible names:
<entity class="User">
<attributes>
<element-collection name="dashboards">
<map-key-join-column name="Dashboard_id" />
<column name="is_default" />
</element-collection>
</attributes>
</entity>

Categories

Resources