MyBatis update if a field in the source object is not null - java

Using MyBatis annotation and dynamic SQL, I was trying to update a field of a table only if the corresponding field in the source object is not null. For example, if #{srcField} is not null, set field=#{srcField}. I do not care if the field in DB is null or not.
However, I failed to come up with a correct dynamic SQL.
I tried two dynamic sql approaches. Approach 1 can be taken by my program, but it failed to check if the specific field in the source object is null. Approach 2 causes some run-time error.
My DAO object with setter/getter (source object to update the table):
#Data
public class CityDO {
private Long id;
private String name;
private String description;
}
Mapper using annotation and dynamic sql. I was trying to update the description field, only if record.description is not null.
Approach 1, it failed the test #{description}!=null, even if record.description==null. Each time the produced sql include SET description = ?.
#Update({
"<script>",
"update city",
"<set>",
"<if test='#{description} != null'>description = #{description,jdbcType=VARCHAR},</if>",
"</set>",
"where name = #{name,jdbcType=VARCHAR}",
"</script>"
})
#Override
int update(CityDO record);
Approach 2, the only difference is I added jdbcType in the test condition. The exception is Caused by: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'VARCHAR' in 'class com.mycode.CityDO'
"<if test='#{description,jdbcType=VARCHAR} != null'>description = #{description,jdbcType=VARCHAR},</if>",
I appreciate if someone can tell me how to make it working, and if there is a reference solution (even an xml-based one is fine).

The following should work.
<if test='description != null'>
The value of test attribute is evaluated by OGNL while #{} is a MyBatis way of referencing parameter/property.
And, in your case, jdbcTypes may be unnecessary (there is no harm, though).

Related

Need to access real column name from Hibernate Implicit naming strategy

I'm implementing a custom hibernate JPA naming strategy in a Hibernate 5.4.32 model.
The implicit naming strategy is setup to convert camelCase property names to snake_case. Thus a property called myKey will turn into a column called my_key. This part works correct, and all columns have the correct names and all JPA interactions work.
However I have an issue with generating index names. In that Strategy interface there is a method:
public Identifier determineIndexName(ImplicitIndexNameSource source)
The parameter "source" passed in that method is ambiguous in how it handles the column names that are passed to the method.
For instance if I create the following entity
#Table(schema = LoaderModel.SCHEMA, name = "main_entity",indexes = {#Index(columnList = "other_id,myKey")} )
#Entity
public class MainEntity{
#ManyToOne
OtherEntity other;
#Column(columnDefinition = "text")
String myKey;
}
This entity creates a table with two columns, other_id, and my_key. But notice how in the #Index I must use the property name "myKey", even though for the ManyToOne column I have to use the actual column name: "other_id". This is in line with what I see in the determineIndexName method:
#Override
public Identifier determineIndexName(ImplicitIndexNameSource source) {
String name = "ix_"+source.getTableName().getText()+"_"+String.join("_",source.getColumnNames().stream().map(Identifier::getText).collect(Collectors.toList()));
//The name here is: ix_main_entity_other_id_myKey
//instead of the desired: ix_main_entity_other_id_my_key
return Identifier.toIdentifier(name);
}
I can't find any properties of the ImplicitIndexNameSource object that allow me to detect the actual column names of the properties in the index. In addition I'm not sure why it passes the column name for entity references, but the property name for entity properties.
I'm guessing some portion of this weird ambiguity is intended, but If I could simply get access to the real column name then I could write the code to handle it.
Note: Manually configuring the column name "my_key" for the myKey property seems to fix this issue, but that defeats the purpose of creating the naming strategy (which would be applied to the entire model), so that's not a valid fix.

Retrieve a result from a stored procedure in a Java object [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.

ORMLite ID field cannot be compared

I have several classes, all of which have an ID field declared as Integer the next way:
#Expose
#DatabaseField(columnName = "_id", id = true)
private Integer idField;
Everything compiles and runs correctly, but when I simply try to check if a record exists:
Integer idField = 1;
result = DBHelper.getHelper().getClassDAO().idExists(idField);
I get the exception:
java.sql.SQLException: Field '_id' is of data type null which can not be compared
The thing is that with one class (let's name it A) the method works properly, but with the others fail and I don't know which is the cause because all the classes have its ID field declared the same way.
I'm getting this exception too if I try to createOrUpdate the object of any class, except the refered class A.
Any helping hand would be appreciated.
NOTE. The project uses an ormlite_config.txt file, which is updated.
After a few hours debugging I came into that the mentioned exception comes only when the class to be saved has one o more collections. As far as I know, the exception is thrown inside the idExists method of the Dao class. Having in mind that the createOrSave method surely must call it, that's why I get the SQLException in both cases.
And now, the workaround to solve this. Rather than invoking the idExists method, I had to create my own like this:
return DBHelper.getHelper().getMyDAO().queryForId(idField) != null;
And instead of calling the createOrUpdate method, first I check if the record exists and then, depending on the result, I call the create or update method of the Dao class I want to persist.

How can I use capitalized property in a hibernate entity

In our product we use auto generated hibernate entities to be able to link a customizable Database scheme to our server software. The entity names and property names are taken from the data base. Especially, the property names can usually not be changed as they also are used in user code unrelated to the hibernate data layer (e.g. python scripts)
Some of these property names are capitalized, which seems to cause some problems. HQL statements using those property names fail with an Exception, e.g.:
org.hibernate.QueryException: could not resolve property List_id
at org.hibernate.QueryException.generateQueryException(QueryException.java:137)
at org.hibernate.QueryException.wrapWithQueryString(QueryException.java:120)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:234)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.compile(QueryTranslatorImpl.java:158)
at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:126)
at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:88)
at org.hibernate.engine.query.spi.QueryPlanCache.getHQLQueryPlan(QueryPlanCache.java:190)
Some code snippet for the example Exception:
#Entity(name = "ListItem")
#Table(name = "LIST_ITEM")
public class ListItem
extends HibernatePojoClass
{
private String List_id = "";
#Column(name = "`LIST_ID`", length = 8)
public String getList_id() {
return List_id;
}
public void setList_id(String List_id) {
this.List_id = List_id;
}
...
and the HQL statement:
select li.id, li.List_id from ListItem li
The exception occurs when hibernate tries to transform the hql statement to a sql statement.
Why does this happen?
It seems that when I use li.list_id in the hql statement, the property is resolved (while this leads to another error); can I prevent this implicit "capitalization change" somehow?
if you use
#Column(name = "`LIST_ID`", length = 8)
public String getList_id() {
return List_id;
}
you should refer that property as list_id in HQL, of course.
Hibernate can use a naming strategy to generate column names. ImprovedNamingStrategy from Hibernate 4 will convert column name to the lower case, even if you specify it. I am not sure about the quotes, but for this:
#Column(name = "LIST_ID", length = 8)
public String getList_id() {
return List_id;
}
using ImprovedNamingStrategy you will have list_id column name.
You can try to use your own naming strategy to generate correct column names.
JPA has 2 basic access modes: property access and field access.
Property access requires you to adhere to the Java Beans convention which means you need field name that starts with a lower case character and a corresponding getter/setter which has the same character in upper case, i.e. field listId would require a getter getListId().
Thus you'd need to use field access in order to have Hibernate use the field name as it is. Another advantage of using field access on an entity's id would be that you'd not need to do any lazy loading in order to just get the id - which wouldn't be possible with property access in Hibernate.
For more information have a look at sections 2.2 and 2.3 of the JPA specification.
A final word of advice though: as already stated multiple times in my comments you should try and stick with the Java code conventions. Some advantages of doing so:
It'll be easier to communicate with others such as people here on SO (e.g. a name starting with a capital letter normally is assumed to be a class name).
You'll have less problems with libraries in the Java eco system since most of them use the same conventions or are based on them (e.g. JavaBeans, JavaEL, etc.)
It'll be easier to spot errors, e.g. when using a class rather than a field or variable etc.
You'll be less dependent on IDE features like code coloring, error highlighting etc.

How to return ids on Inserts with mybatis in mysql with annotations

See this related question for Postgres. For some reason, the solution doesn't work for me - the return value of the insert statement is always "1".
See this other question for an XML based solution. I would like to do the same without XML - insert a record and find the new auto-generated id of the record I just insreted.
I didn't find a matching annotation to <selectkey> (see this open issue)
How do I proceed?
Examining mybatis code reveals that INSERT is implemented via UPDATE, and always returns the number of inserted rows! So ... unless I'm completely missing something here, there's no way to do this using the current (3.0.3) implementation.
Actually, it's possible to do it, with the #Options annotation (provided you're using auto_increment or something similar in your database) :
#Insert("insert into table3 (id, name) values(null, #{name})")
#Options(useGeneratedKeys=true, keyProperty="idName")
int insertTable3(SomeBean myBean);
Note that the keyProperty="idName" part is not necessary if the key property in SomeBean is named "id". There's also a keyColumn attribute available, for the rare cases when MyBatis can't find the primary key column by himself. Please also note that by using #Options, you're submitting your method to some default parameters ; it's important to consult the doc (linked below -- page 60 in the current version) !
(Old answer) The (quite recent) #SelectKey annotation can be used for more complex key retrieval (sequences, identity() function...). Here's what the MyBatis 3 User Guide (pdf) offers as examples :
This example shows using the #SelectKey annotation to retrieve a value from a sequence before an
insert:
#Insert("insert into table3 (id, name) values(#{nameId}, #{name})")
#SelectKey(statement="call next value for TestSequence", keyProperty="nameId", before=true, resultType=int.class)
int insertTable3(Name name);
This example shows using the #SelectKey annotation to retrieve an identity value after an insert:
#Insert("insert into table2 (name) values(#{name})")
#SelectKey(statement="call identity()", keyProperty="nameId", before=false, resultType=int.class)
int insertTable2(Name name);
The <insert>, <update>and <delete> statements return the number of affected rows, as is common with database APIs.
If a new ID is generated for the inserted row, it is reflected in the object you passed as a parameter. So for example, if you call mapper.insert(someObject) inside your annotated insert method, after inserting, you can call someObject.getId (or similar) to retrieve it.
Using the options of <insert>, you can tweak how (by providing an SQL statement) and when (before or after the actual insertion) the id is generated or retrieved, and where in the object it is put.
It may be instructive to use the MyBatis generator to generate classes from a database schema and have a look at how inserts and updates are handled. Specifically, the generator produces "example" classes that are used as temporary containers to pass around data.
you can get your generated ids from save methods,
lets say a bean with ID and name properties,
bean.setName("xxx");
mapper.save(bean);
// here is your id
logger.debug(bean.getID);
I didn't like most of the answers I found online for returning generated keys because
All of the solutions I found called a "setter" on the inbound object
None of the solutions returned the generated column from the method
I came up with the following solution which addresses points 1 & 2 above which
Passes two parameters to mybatis "in" & "out" (mybatis does not mutate "in", it calls a setter on "out")
Requires an additional default method on the interface to return the value
public interface MyMapper {
/**
* this method is used by the mybatis mapper
* I don't call this method directly in my application code
*/
#Insert("INSERT INTO MY_TABLE (FOO) VALUES ({#in.foo})")
#Options(useGeneratedKeys=true, keyColumn="ID", keyProperty = "out.value")
void insert(#Param("in") MyTable in, #Param("out") LongReference out);
/**
* this "default method" is called in my application code and returns the generated id.
*/
default long insert(MyTable tableBean) {
LongReference idReference = new LongReference();
insert(tableBean, idReference);
return idReference.getValue();
}
}
This requires an additional class which can be re-used on similar methods in future
public class LongReference {
private Long value;
// getter & setter
}

Categories

Resources