MyBatis enum usage - java

I know this has been asked before, but I wasn't able to implement a solution based on the information I found so far. so perhaps someone can explain it to me.
I have a table "status". It has two columns:id and name. id is a PK.
Instead of using a POJO Status, I would like to use an enum. I created such an enum as follows:
public enum Status {
NEW(1), READY(2), CLOSED(3);
private int id;
public void setId(int id) {
this.id = id;
}
public int getId() {
return this.id;
}
Status(int id) {
this.id = id;
}
}
here is my mapper
<select id="getStatusByName" resultType="Status" parameterType="String">
SELECT ls.id, ls.name
FROM status AS ls
WHERE ls.name = #{name}
</select>
but for some reason, when I try to retrieve an enum, something breaks, but no exception is thrown.

I have worked on this question from a couple of angles and here are my findings. Caveat: I did all these investigations using MyBatis-3.1.1, so things might have behaved differently in earlier versions.
First, MyBatis has a built-in EnumTypeHandler. By default, any time you specify a Java enum as a resultType or parameterType, this is what will handle that type. For queries, when trying to convert a database record into a Java enum, the EnumTypeHandler only takes one argument and tries to look up the Java enum value that corresponds to that value.
An example will better illustrate. Suppose your query above returns 2 and "Ready" when I pass in "Ready" as the argument. In that case, I get the error message No enum constant com.foo.Status.2. If I reverse the order of your SELECT statement to be
SELECT ls.name, ls.id
then the error message is No enum constant com.foo.Status.Ready. I assume you can infer what MyBatis is doing. Note that the EnumTypeHandler is ignoring the second value returned from the query.
Changing your query to
SELECT UPPER(ls.name)
causes it to work: the Status.READY enum is returned.
So next I tried to define my own TypeHandler for the Status enum. Unfortunately, as with the default EnumTypeHandler, I could only get one of the values (id or name) in order to reference the right Enum, not both. So if the database id does not match the value you hardcoded above, then you will have a mismatch. If you ensure that the database id always matches the id you specify in the enum, then all you need from the database is the name (converted to upper case).
Then I thought I'd get clever and implement a MyBatis ObjectFactory, grab both the int id and String name and ensure those are matched up in the Java enum I pass back, but that did not work as MyBatis does not call the ObjectFactory for a Java enum type (at least I couldn't get it to work).
So my conclusion is that Java enums in MyBatis are easy as long as you just need to match up the name from the database to the enum constant name - either use the built-in EnumTypeHandler or define your own if doing UPPER(name) in the SQL isn't enough to match the Java enum names. In many cases, this is sufficient, as the enumerated value may just be a check constraint on a column and it has only the single value, not an id as well. If you need to also match up an int id as well as a name, then make the IDs match manually when setting up the Java enum and/or database entries.
Finally, if you'd like to see a working example of this, see koan 23 of my MyBatis koans here: https://github.com/midpeter444/mybatis-koans. If you just want to see my solution, look in the completed-koans/koan23 directory. I also have an example there of inserting a record into the database via a Java enum.

You can use Custom TypeHandler for converting you result directly into ENUM so that you don't need to put all values in your database as UPPER CASE ENUM Names.
This is how your Status Enum Custom Handler will look like
public class StatusTypeHandler implements TypeHandler<Status> {
public Status getResult(ResultSet rs, String param) throws SQLException {
return Status.getEnum(rs.getInt(param));
}
public Status getResult(CallableStatement cs, int col) throws SQLException {
return Status.getEnum(cs.getInt(col));
}
public void setParameter(PreparedStatement ps, int paramInt, Status paramType, JdbcType jdbctype)
throws SQLException {
ps.setInt(paramInt, paramType.getId());
}
}
Define your TypeHandler to handle Status by default in your mybatis-config.xml by adding this code.
<typeHandlers>
<typeHandler javaType='Status' handler='StatusTypeHandler' />
</typeHandlers>
Now let us consider an example where you have following two functions in your Dao,
Status getStatusById(int code);
Status getStatusByName(String name);
Your mapper will look like
<select id="getStatusById" resultType="Status" parameterType="int">
SELECT ls.id
FROM status AS ls
WHERE ls.id = #{id}
</select>
<select id="getStatusByName" resultType="Status" parameterType="String">
SELECT ls.id
FROM status AS ls
WHERE ls.name = #{name}
</select>
Now as the resultType for both the mapper is Status, myBatis will use the CustomTypeHandler for this type i.e. StatusTypeHandler instead of EnumTypeHandler that it uses by default for Handling Enums, so there would be no need to maintain proper Enum names in your database.

Related

Using Blaze Persistence optional parameter in subquery

In Blaze Persistence, is there a way to use an optional parameter in a subquery?
In my current use case, let's say I have topics, users, and a third table called topic_last_seen to record the date at which each user has last read each topic. I would like to create an entity view for the Topic entity which also maps that date for each topic with the current user given as an "optional parameter". This is roughly what I've been trying:
#EntityView(Topic.class)
public interface TopicView
{
#IdMapping
Long getId();
String getSummary();
#MappingParameter("currentUserId")
Long getCurrentUserId();
#MappingSubquery(LastSeenDateSubqueryProvider.class)
Date getLastSeenDate();
class LastSeenDateSubqueryProvider implements SubqueryProvider
{
#Override
public <T> T createSubquery(SubqueryInitiator<T> subqueryBuilder)
{
return subqueryBuilder.from(TopicLastSeen.class, "lastSeen")
.select("dateSeen")
.where("lastSeen.user.id").eqExpression("EMBEDDING_VIEW(currentUserId)")
.where("lastSeen.topic.id").eqExpression("EMBEDDING_VIEW(id)")
.end();
}
}
}
Unfortunately, this results in an exception:
java.lang.IllegalArgumentException: Attribute 'currentUserId' not found on type 'Topic'
Is there any way to use an optional parameter in a subquery like this at all? Or is there a different way to map this information? Changing the DB schema is not an option at this point.
Looks like I figured this out a possible suolution by trial and error. It seems that the "optional parameters" from EntityViewSetting can be used like regular parameters in JPQL:
return subqueryBuilder.from(TopicLastSeen.class, "lastSeen")
.select("dateSeen")
.where("lastSeen.user.id").eqExpression(":currentUserId")
.where("lastSeen.topic.id").eqExpression("EMBEDDING_VIEW(id)")
.end();
Unfortunately though, with this subquery mapping the currentUserId parameter is no longer "optional". When it is not set I get another exception:
org.hibernate.QueryException: Named parameter not bound : currentUserId

Set value to one of the property in Java 15 record

I am using Java 15 preview feature record in my code, and defined the record as follow
public record ProductViewModel
(
String id,
String name,
String description,
float price
) {
}
In the controller level I have the below code
#Put(uri = "/{id}")
public Maybe<HttpResponse> Update(ProductViewModel model, String id) {
LOG.info(String.format("Controller --> Updating the specified product"));
return iProductManager.Update(id, model).flatMap(item -> {
if(item == null)
return Maybe.just(HttpResponse.notFound());
else
return Maybe.just(HttpResponse.accepted());
});
}
From the UI in the model the value of id is not passed, however, it is passed as a route parameter. Now I want to set the value in the controller level, something like
model.setid(id) // Old style
How can I set the value to the record particular property
You can't. Record properties are immutable. What you can do however is add a wither to create a new record with same properties but a new id:
public record ProductViewModel(String id,
String name,
String description,
float price) {
public ProductViewModel withId(String id) {
return new ProductViewModel(id, name(), description(), price());
}
}
You cannot modify them. From the Oracle page:
A record class is a shallowly immutable, transparent carrier for a
fixed set of values, called the record components. The Java language
provides concise syntax for declaring record classes, whereby the
record components are declared in the record header. The list of
record components declared in the record header form the record
descriptor.
From the Java Language Specification Section 8.10 one can read the following:
A record declaration is implicitly final. It is permitted for the
declaration of a record class to redundantly specify the final
and
8.10.3 Record Members
A record class has for each record component appearing in the record
component list an implicitly declared field with the same name as the
record component and the same type as the declared type of the record
component. This field is declared private and final. The field is
annotated with the annotations, if any, that appear on the
corresponding record component and whose annotation types are
applicable in the field declaration context, or in type contexts, or
both.
If you need to mutate an attribute, then you need to use a class instead of a record.
From the JEP:
Enhance the Java programming language with records, which are classes that act as transparent carriers for immutable data. Records can be thought of as nominal tuples.
So, you'd better use a class if you need that behavior.

How to implement Seed and Next when extending UserVersionType

I'm trying to implement a String based UserVersionType. I have found examples enough to understand how to use the UserType methods to an extent. I can't find anything that shows me exactly what to do with next() and seed().
So I have something like this
public class StringVersionType implements UserType, UserVersionType {
...
public int compare(Object o1, Object o2) {
String a = (String) o1;
String b = (String) o2;
return a.compareTo(b);
}
public Object next(Object arg0, SharedSessionContractImplementor arg1)
{
return "DUMMY SEED"; // + LocalTime.now().toString();
}
public Object seed(SharedSessionContractImplementor session){
return "DUMMY SEED"; // LocalTime.now().toString();
}
}
I've tried adding simple code to return a string that is always the same and code that might change the version number. I always get an error on update. Looking at the hibernate console output when I add almost anything to these UserVersionType methods hibernate stops doing a select and then an update but always goes straight to a new insert query and so fails on a primary key still exists.
Obviously I'm misunderstanding what seed and next should do but I can't find any useful documentation ?
Can anyone tell me more about how to use them ?
Seed:
Generate an initial version.
Parameters:
session - The session from which this request originates. May be null; currently this only happens during startup when trying to determine the "unsaved value" of entities.
Returns:
an instance of the type
#Override
public Object
seed(SharedSessionContractImplementor session)
{
return ( (UserVersionType) userType ).seed(
session );
}
For properties mapped as either version or timestamp, the insert statement gives you two options. You can either specify the property in the properties_list, in which case its value is taken from the corresponding select expressions, or omit it from the properties_list, in which case the seed value defined by the org.hibernate.type.VersionType is used
next:
Increment the version.
Parameters:
session - The session from which this request originates.
current - the current version
Returns:
an instance of the type
public Object next(Object current,
SessionImplementor session) {
return ( (UserVersionType) userType ).next( current, session );
}
From the docs:
"UPDATE statements, by default, do not effect the version or the timestamp attribute values for the affected entities. However, you can force Hibernate to set the version or timestamp attribute values through the use of a versioned update. This is achieved by adding the VERSIONED keyword after the UPDATE keyword. Note, however, that this is a Hibernate specific feature and will not work in a portable manner. Custom version types, org.hibernate.usertype.UserVersionType, are not allowed in conjunction with a update versioned statement."
Other Docs:
Dedicated version number
The version number mechanism for optimistic locking is provided through a #Version annotation.
The #Version annotation
#Entity
public class Flight implements Serializable {
...
#Version
#Column(name="OPTLOCK")
public Integer getVersion() { ... }
}
Here, the version property is mapped to the OPTLOCK column, and the entity manager uses it to detect conflicting updates, and prevent the loss of updates that would be overwritten by a last-commit-wins strategy.
The version column can be any kind of type, as long as you define and implement the appropriate UserVersionType.
Your application is forbidden from altering the version number set by Hibernate. To artificially increase the version number, see the documentation for properties LockModeType.OPTIMISTIC_FORCE_INCREMENT or LockModeType.PESSIMISTIC_FORCE_INCREMENTcheck in the Hibernate Entity Manager reference documentation.
Database-generated version numbers
If the version number is generated by the database, such as a trigger, use the annotation #org.hibernate.annotations.Generated(GenerationTime.ALWAYS).
Declaring a version property in hbm.xml
<version
column="version_column"
name="propertyName"
type="typename"
access="field|property|ClassName"
unsaved-value="null|negative|undefined"
generated="never|always"
insert="true|false"
node="element-name|#attribute-
name|element/#attribute|."
/>
This is all I can find from the documentation that can help you understand why and how to use those methods. Give me feed back about the irrelevent parts, due to my misunderstanding to the question, to remove it.

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.

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