How control mybatis‘s localCache? - mybatis

I use mybatis version 3.4.0 in my project with postgre sql. There is a id column
is serial type (auto increate primary key column).
I write the mapper's generateId method to get the serial value:
mapper interface:
public interface DemoTableNameMapper {
long generateId();
...
mapper.xml:
<select id="generateId" resultType="long">
SELECT nextval('"public".demo_table_name_id_seq'::regclass)
</select>
But when I call the mapper's generateId method twice want to get two ID value, I find I get the SAME value!!!
Then I trace into mybatis's source code, I found it's mybatis' localCache return the previous generated ID value.
To work around with this problem, I change my generate's declare and implementation.
interface:
public interface DemoTableNameMapper {
/**
* 产生新的主表的ID
* 注:因在一次事务中需多次调用,需通过混淆参数来避免命中 mybatis 的 localCache
* #return
*/
long generateId(#Param("dummyid") String dummyid);
...
mapper.xml:
<select id="generateId" resultType="long">
SELECT nextval('"public".demo_table_name_id_seq'::regclass)
WHERE #{dummyid}=#{dummyid}
</select>
My question is:
Is there a better way to interact with mybatis's localCache?
I change mapper.xml this way:
<select id="generateId" resultType="long" useCache="false" flushCache="true">
SELECT nextval('"public".demo_table_name_id_seq'::regclass)
</select>
it work!!!

By Default, the localSessionScope is session - So If you make the call twice or N times till the cache clears in the session, it returns you the same value.
If you set localSessionScope as STATEMENT. Every time the new next val is returned.
localCacheScope=STATEMENT
Alternative way is to clear Cache - (make sure if its ok for the performance), after the first call - by calling
clearCache()
Refer Here for complete details - MyBatis Documentation

Related

Spring JPA native query to '#IdClass' annotated table and getting "No Dialect mapping for JDBC type: 1111" [duplicate]

I'm working on a Spring JPA Application, using MySQL as database. I ensured that all spring-jpa libraries, hibernate and mysql-connector-java is loaded.
I'm running a mysql 5 instance. Here is a excerpt of my application.properties file:
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
spring.datasource.url=jdbc:mysql://localhost/mydatabase
spring.datasource.username=myuser
spring.datasource.password=SUPERSECRET
spring.datasource.driverClassName=com.mysql.jdbc.Driver
When executing an integration test, spring startsup properly but fails on creating the hibernate SessionFactory, with the exception:
org.hibernate.MappingException: No Dialect mapping for JDBC type: 1111
I think my dialects should be Mysql5Dialect, I also tried the one explicitly stating InnoDB, and the two dialect options which don't indicate the version 5. But I always end up with the same 'No Dialect mapping for JDBC type: 1111' message.
My application.properties file resides in the test/resources source folder. It is recognized by the JUnit Test runner (I previously got an exception because of an typo in it).
Are the properties I'm setting wrong? I couldn't find some official documentation on these property names but found a hint in this stackoverflow answer: https://stackoverflow.com/a/25941616/1735497
Looking forward for your answers, thanks!
BTW The application is already using spring boot.
I got the same error because my query returned a UUID column. To fix that I returned the UUID column as varchar type through the query like "cast(columnName as varchar)", then it worked.
Example:
public interface StudRepository extends JpaRepository<Mark, UUID> {
#Modifying
#Query(value = "SELECT Cast(stuid as varchar) id, SUM(marks) as marks FROM studs where group by stuid", nativeQuery = true)
List<Student> findMarkGroupByStuid();
public static interface Student(){
private String getId();
private String getMarks();
}
}
Here the answer based on the comment from SubOptimal:
The error message actually says that one column type cannot be mapped to a database type by hibernate.
In my case it was the java.util.UUID type I use as primary key in some of my entities. Just apply the annotation #Type(type="uuid-char") (for postgres #Type(type="pg-uuid"))
There is also another common use-case throwing this exception. Calling function which returns void. For more info and solution go here.
I got the same error, the problem here is UUID stored in DB is not converting to object.
I tried applying these annotations #Type(type="uuid-char") (for postgres #Type(type="pg-uuid") but it didn't work for me.
This worked for me. Suppose you want id and name from a table with a native query in JPA. Create one entity class like 'User' with fields id and name and then try converting object[] to entity we want. Here this matched data is list of array of object we are getting from query.
#Query( value = "SELECT CAST(id as varchar) id, name from users ", nativeQuery = true)
public List<Object[]> search();
public class User{
private UUID id;
private String name;
}
List<User> userList=new ArrayList<>();
for(Object[] data:matchedData){
userList.add(new User(UUID.fromString(String.valueOf(data[0])),
String.valueOf(data[1])));
}
Suppose this is the entity we have
Please Check if some Column return many have unknow Type in Query .
eg : '1' as column_name can have type unknown
and 1 as column_name is Integer is correct One .
This thing worked for me.
Finding the column that triggered the issue
First, you didn't provide the entity mapping so that we could tell what column generated this problem. For instance, it could be a UUID or a JSON column.
Now, you are using a very old Hibernate Dialect. The MySQL5Dialect is meant for MySQL 5. Most likely you are using a newer MySQL version.
So, try to use the MySQL8Dialect instead:
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
Adding non-standard types
In case you got the issue because you are using a JSON column type, try to provide a custom Hibernate Dialect that supports the non-standard Type:
public class MySQL8JsonDialect
extends MySQL8Dialect{
public MySQL8JsonDialect() {
super();
this.registerHibernateType(
Types.OTHER, JsonStringType.class.getName()
);
}
}
Ans use the custom Hibernate Dialect:
<property
name="hibernate.dialect"
value="com.vladmihalcea.book.hpjp.hibernate.type.json.MySQL8JsonDialect"
/>
If you get this exception when executing SQL native queries, then you need to pass the type via addScalar:
JsonNode properties = (JsonNode) entityManager
.createNativeQuery(
"SELECT properties " +
"FROM book " +
"WHERE isbn = :isbn")
.setParameter("isbn", "978-9730228236")
.unwrap(org.hibernate.query.NativeQuery.class)
.addScalar("properties", JsonStringType.INSTANCE)
.getSingleResult();
assertEquals(
"High-Performance Java Persistence",
properties.get("title").asText()
);
Sometimes when you call sql procedure/function it might be required to return something. You can try returning void: RETURN; or string (this one worked for me): RETURN 'OK'
If you have native SQL query then fix it by adding a cast to the query.
Example:
CAST('yourString' AS varchar(50)) as anyColumnName
In my case it worked for me.
In my case, the issue was Hibernate not knowing how to deal with an UUID column. If you are using Postgres, try adding this to your resources/application.properties:
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL9Dialect
Another simple explanation might be that you're fetching a complex Type (Entity/POJO) but do not specify the Entity to map to:
String sql = "select yourentity.* from {h-schema}Yourentity yourentity";
return entityManager.createNativeQuery(sql).getResultList();
simply add the class to map to in the createNativeQuery method:
return entityManager.createNativeQuery(sql, Yourentity.class).getResultList();
In my case the problem was that, I forgot to add resultClasses attribute when I setup my stored procedure in my User class.
#NamedStoredProcedureQuery(name = "find_email",
procedureName = "find_email", resultClasses = User.class, //<--I forgot that.
parameters = {
#StoredProcedureParameter(mode = ParameterMode.IN, name = "param_email", type = String.class)
}),
This also happens when you are using Hibernate and returning a void function. AT least w/ postgres. It doesnt know how to handle the void. I ended up having to change my void to a return int.
If you are using Postgres, check that you don't have a column of type Abstime. Abstime is an internal Postgres datatype not recognized by JPA. In this case, converting to Text using TO_CHAR could help if permitted by your business requirements.
if using Postgres
public class CustomPostgreSqlDialect extends PostgreSQL94Dialect{
#Override
public SqlTypeDescriptor remapSqlTypeDescriptor(SqlTypeDescriptor sqlTypeDescriptor)
{
switch (sqlTypeDescriptor.getSqlType())
{
case Types.CLOB:
return VarcharTypeDescriptor.INSTANCE;
case Types.BLOB:
return VarcharTypeDescriptor.INSTANCE;
case 1111://1111 should be json of pgsql
return VarcharTypeDescriptor.INSTANCE;
}
return super.remapSqlTypeDescriptor(sqlTypeDescriptor);
}
public CustomPostgreSqlDialect() {
super();
registerHibernateType(1111, "string");
}}
and use
<prop key="hibernate.dialect">com.abc.CustomPostgreSqlDialect</prop>
For anybody getting this error with an old hibernate (3.x) version:
do not write the return type in capital letters. hibernate type implementation mapping uses lowercase return types and does not convert them:
CREATE OR REPLACE FUNCTION do_something(param varchar)
RETURNS integer AS
$BODY$
...
This is for Hibernate (5.x) version
Calling database function which return JSON string/object
For this use unwrap(org.hibernate.query.NativeQuery.class).addScalar() methods for the same.
Example as below (Spring & Hibernate):
#PersistenceContext
EntityManager em;
#Override
public String getJson(String strLayerName) {
String *nativeQuery* = "select fn_layer_attributes(:layername)";
return em.createNativeQuery(*nativeQuery*).setParameter("layername", strLayerName).**unwrap(org.hibernate.query.NativeQuery.class).addScalar**("fn_layer_attributes", **new JsonNodeBinaryType()**) .getSingleResult().toString();
}
Function or procedure returning void cause some issue with JPA/Hibernate, so changing it with return integer and calling return 1 at the end of procedure may solved the problem.
SQL Type 1111 represents String.
If you are calling EntityManager.createNativeQuery(), be sure to include the resulting java class in the second parameter:
return em.createNativeQuery(sql, MyRecord.class).getResultList()
After trying many proposed solutions, including:
https://stackoverflow.com/a/59754570/349169 which is one of the solutions proposed here
https://vladmihalcea.com/hibernate-no-dialect-mapping-for-jdbc-type/
it was finally this one that fixed everything with the least amount of changes:
https://gist.github.com/agrawald/adad25d28bf6c56a7e4618fe95ee5a39
The trick is to not have #TypeDef on your class, but instead have 2 different #TypeDef in 2 different package-info.java files. One inside your production code package for your production DB, and one inside your test package for your test H2 DB.

Use an property of an object which is array of strings in mapper XML

I want to refer to a property in an object in my mapper file which is an array of strings in a SQL IN criteria. The query does a count, so all it needs to return is a numeric value. The query needs to adjust its count based on a flexible set of criteria defined in a filter object. Some filters will be present (ie. not null), and others will be absent.
<?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="TotalUniqueUsers">
<select id="getTotalUniqueUsers"
resultType="int"
parameterType="RequestFilter">
SELECT *
FROM MY_TABLE
WHERE
<if test="quarterList!=null and quarterList.length>0">
AND trim(FISCAL_QUARTER_NAME) IN #{quarterList javaType=list}
</if>
</select>
</mapper>
public class RequestFilter {
private String[] quarterList;
public String[] getQuarterList(){
return this.quarterList;
}
public void setQuarterList(String[] quarterList){
this.quarterList=quarterList;
}
}
Note, there is no type handler for RequestFilter. I did not think I needed one. I'm not trying to take an object and condense it into say one field in some weird way. All I want to do is have an input parameter to the
With the above, I get
org.apache.ibatis.exceptions.PersistenceException:
...
Caused by: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'quarterList javaType=list' in 'class RequestFilter'
I tried javaType=Array also, but get the same result. If I change the
#{quarterList javaType=list}
to
#{quarterList}
it says the typeHandler is null for the RequestFilter.
There is no portable way to set the list or array to IN prepared statement parameter in JDBC and therefore in mybatis (there are ways to do that if you are using postgres).
So in the general case you need to dynamically generate the query with a parameter per element in the list:
<select id="getTotalUniqueUsers"
resultType="int"
parameterType="RequestFilter">
SELECT *
FROM MY_TABLE
WHERE
<if test="quarterList!=null and quarterList.length>0">
trim(FISCAL_QUARTER_NAME) IN (
<foreach item='quarter' collection='quarterList' separator=','>
#{quarter}
</foreach>
)
</if>
</select>

Mybatis: no Result Maps were found for the Mapped Statement

I had to add a ping query to ensure db did not timeout, to existing mapper files and code, this looks as follows:
In the mapper XML:
<select id="pingTest" statementType="CALLABLE">
SELECT 1 FROM DUAL
</select>
Interface:
void pingTest() throws SQLException;
Implementation (inside LogInputDao class):
public void pingTest() throws SQLException {
SqlSession session = null;
SqlSessionFactory sqlSessionFactory = MyBatisDBConfig.getSqlSessionFactory();
session = sqlSessionFactory.openSession();
LogInputMapper mapper = session.getMapper(LogInputMapper.class);
mapper.pingTest();
}
logInputDao.pingTest()
Brings out the error:
### Error querying database. Cause: org.apache.ibatis.executor.ExecutorException: A query was run and no Result Maps were found for the Mapped Statement 'com.example.proj.name.dbservice.mappers.LogInputMapper.pingTest'. It's likely that neither a Result Type nor a Result Map was specified.
### The error may exist in com/example/proj/dbservice/config/LogInputMapper.xml
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: SELECT 1 FROM DUAL
### Cause: org.apache.ibatis.executor.ExecutorException: A query was run and no Result Maps were found for the Mapped Statement 'com.example.proj.name.dbservice.mappers.LogInputMapper.pingTest'. It's likely that neither a Result Type nor a Result Map was specified.
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:23)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:104)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:95)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:59)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:95)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:40)
at com.sun.proxy.$Proxy18.pingTest(Unknown Source)
at com.example.proj.name.dbservice.dao.LogInputDao.pingTest(LogInputDao.java:52)
.....
I dont need the results, either way I am not sure exactly how to fix this, first time I do something with MyBatis
As #ave said already above,
in select statements MyBatis expects some data to return and it needs to map it.
One of them is necessary.
So you have to add a resultType or resultMap tag and remove unnecessary statementType="CALLABLE".
Here is an example of mapping:
<select id="pingTest" resultType="int">
SELECT 1 FROM DUAL
</select>

Difference between modelAttribute and commandName attributes in form tag in spring?

In Spring 3, I have seen two different attribute in form tag in jsp
<form:form method="post" modelAttribute="login">
in this the attribute modelAttribute is the name of the form object whose properties are used to populate the form. And I used it in posting a form and in controller I have used #ModelAttribute to capture value, calling validator, applying business logic. Everything is fine here. Now
<form:form method="post" commandName="login">
What is expected by this attribute, is it also a form object whose properties we are going to populate?
If you look at the source code of FormTag (4.3.x) which backs your <form> element, you'll notice this
/**
* Set the name of the form attribute in the model.
* <p>May be a runtime expression.
*/
public void setModelAttribute(String modelAttribute) {
this.modelAttribute = modelAttribute;
}
/**
* Get the name of the form attribute in the model.
*/
protected String getModelAttribute() {
return this.modelAttribute;
}
/**
* Set the name of the form attribute in the model.
* <p>May be a runtime expression.
* #see #setModelAttribute
*/
public void setCommandName(String commandName) {
this.modelAttribute = commandName;
}
/**
* Get the name of the form attribute in the model.
* #see #getModelAttribute
*/
protected String getCommandName() {
return this.modelAttribute;
}
They are both referring to the same field, thus having same effect.
But, as the field name indicates, modelAttribute should be preferred, as others have also pointed out.
OLD WAY = commandName
...
<spring:url value="/manage/add.do" var="action" />
<form:form action="${action}" commandName="employee">
<div>
<table>
....
NEW WAY = modelAttribute
..
<spring:url value="/manage/add.do" var="action" />
<form:form action="${action}" modelAttribute="employee">
<div>
<table>
..
I had the same question a while ago, I can't remember the exact differences but from research I ascertained that commandName was the old way of doing it and in new applications you should be using modelAttribute
commandName = name of a variable in the request scope or session scope that contains the information about this form,or this is model for this view. Tt should be a been.
In xml based config, we will use command class to pass an object between controller and views. Now in annotation we are using modelattribute.

MyBatis: How to map "inverse" relationship?

My problem is to persist two classes that have a 1:n relationship:
public class DayRecord {
private Long id;
private List<TimeRecord> timeRecordsToday = new ArrayList<TimeRecord>(4);
...
}
public class TimeRecord {
private Long id;
...
}
So, in code, DayRecord knows TimeRecord.
create table DAY_RECORDS (
id int primary key,
);
create table TIME_RECORDS (
id int primary key,
day_record_id int not null,
foreign key (day_record_id) references DAY_RECORDS (id)
);
In database, TimeRecord knows DayRecord.
Can I save a DayRecord with all its TimeRecords in one step?
In Hibernate, I can set an inverse mapping and just save a DayRecord and all its TimeRecords will get saved, too. With MyBatis, I tried to save the classes independently from each other:
<mapper
namespace="de.stevenschwenke.java.javafx.xyz.DayRecordMapper">
<insert id="insertDayRecord"
parameterType="de.stevenschwenke.java.javafx.xyz.DayRecord">
insert into DAY_RECORDS (id) values (NEXT VALUE FOR DAY_RECORDS_SEQ);
</insert>
</mapper>
<mapper
namespace="de.stevenschwenke.java.javafx.xyz.TimeRecordMapper">
<insert id="insertTimeRecord"
parameterType="de.stevenschwenke.java.javafx.xyz.TimeRecord">
insert into TIME_RECORDS (id) values (NEXT VALUE FOR TIME_RECORDS_SEQ);
</insert>
</mapper>
But how can I save the DayRecord-ID inTimeRecord?
Ideas:
Give TimeRecord an attribute dayRecordId. This way, a cyclic dependency would be created. However, the mapping would take care of the dpenedency while saving.
In one transaction, save the DayRecord first, get its ID, set it in TimeRecords and save this object.
use a nested select-statement within insert like in the documentation
What is the best way to save both objects? Thanks for your help!
As jdevelop already mentioned, MyBatis is just a SQL wrapper. Because SQL doesn't offer a way to insert two objects that have a relationship, MyBatis can't do that either.
So here's my workaround: As I mentioned, I don't want to add a circular dependency by letting TimeRecord know about DayRecord. So I created a wrapper class just for inserting TimeRecords:
public class TimeRecordInsertWrapper {
public Long id;
public int hours;
public long dayRecordId;
[constructor/getter/setter omited but there with public access modifier]
}
First, I store the DayRecord and get it's ID. Then I create the wrapper object and store the TimeRecords:
public long insertDayRecord(DayRecord newRecord) {
SqlSession session = sqlSessionFactory.openSession();
try {
session.insert(
"de.stevenschwenke.java.javafx.xyz.DayRecordMapper.insertDayRecord",
newRecord);
for (TimeRecord tr : newRecord.getTimeRecordsToday()) {
TimeRecordInsertWrapper wrapper = new TimeRecordInsertWrapper(tr.getHours(), newRecord.getId());
session.insert("de.stevenschwenke.java.javafx.xyz.TimeRecordMapper.insertTimeRecord",
wrapper);
}
return newRecord.getId();
} finally {
session.commit();
session.close();
}
}
This way, I can use my nice one-way object model AND have the "right" mapping in the database.
Mybatis is just SQL mapping framework, it allows you to abstract SQL code from Java code and that's it, more or less. They are pretending to look like Hibernate with recent versions, but this leads to weird constructions in XML.
I would suggest to store the DayRecord and get it's it from selectKey, then use that ID in subsequent calls to the mapper. This is what actually happens inside the mapper, but complex XML implies complex FSM to built inside. So keep it simple and you're safe with myBatis, or use Hibernate.
What is even better, you can define custom DAO interfaces for the tasks, and then you can have some sort of Service layer with #Transactional attribute set. This requires mybatis-guice, but it works really great and you don't need to deal with transactions in your code (they are declarative).