Dynamically Add Fields to Hibernate Entity at Runtime - java

I have an application with custom fields - users are basically able to define a custom field by selecting a type for the field and giving it a name. The custom fields are then presented as being part of an entity and data given to these fields is saved to my database. In most circumstances, I've been able to handle these programmatically and through the normal hibernate mappings (ie, #OneToMany annotated collection) without a problem. I'm currently faced with a problem, however. We'd like to have these custom fields and their values used for real-time reporting of the "parent" entities. Custom field values are mapped as collections inside the parent entities, but I need them flat for reporting purposes. I have created a view that provides exactly what I need from the SQL side of things - I followed this example to add dynamic pivoting and the resulting query is precisely how I'd like to display my information. Not the following images, of course, but that's essentially the output I have.
The view returns a completely dynamic number of columns, each named for a custom field and populated with the relevant data for that row.
The problem is that I now have no idea how to retrieve this information with Hibernate.
I found documentation for updating the PersistentClass by getting the ClassMappings from the Hibernate Configuration:
Manipulating metadata at runtime
//Get the existing mapping for AgreementsGrid from Configuration
PersistentClass gridMapping = configuration.getClassMapping(AgreementsGrid.class.getName());
//Define new Column
Column column = new Column();
column.setName("ESTIMATED_COST_OVERRUNS");
column.setNullable(true);
column.setUnique(false);
gridMapping.getTable().addColumn(column);
//Wrap the column in a value
SimpleValue value = new SimpleValue();
value.setTable(gridMapping.getTable());
value.setTypeName("string");
value.addColumn(column);
//Define new property for the AgreementsGrid class
Property prop = new Property();
prop.setValue(value);
prop.setName("customField1");
prop.setNodeName(prop.getName());
gridMapping.addProperty(prop);
//Build a new session factory for the new mapping
SessionFactory sessionFactory = configuration.buildSessionFactory();
I've only just realized that this is for Hibernate 3 & 4, and isn't even possible in Hibernate 5 (I'm using 5.2.18).
So, I'm trying to figure out how to handle this in Hibernate 5. I have a base entity mapped to a view, and at runtime I need to be able to dynamically add "fields" to it, so that my DAOs can dynamically filter the information and handle sorts/grouping.
Here is the entity I have for my view:
#Entity
#Table(name="AGREEMENTS_GRID")
public class AgreementsGrid implements Serializable {
private static final long serialVersionUID = 1L;
private Integer entityId;
#Column(name="ENTITY_ID")
#Id
public Integer getEntityId() {
return this.entityId;
}
public void setEntityId(Integer entityId) {
this.entityId = entityId;
}
private Agreements agreement;
#ManyToOne
#JoinColumn(name = "AGREEMENT_ID", referencedColumnName = "ID", nullable = false)
public Agreements getAgreement() {
return this.agreement;
}
public void setAgreement(Agreements agreement) {
this.agreement= agreement;
}
private BigDecimal expenditure;
#Column(name = "EXPENDITURE", nullable = true, precision = 22, scale = 2)
public BigDecimal getExpenditure() {
return this.expenditure;
}
public void setExpenditure(BigDecimal expenditure) {
this.expenditure = expenditure;
}
/*
* Dynamic fields would theoretically go here and look like this,
* for a custom field of type CURRENCY named 'Estimated Cost Overruns'
*/
/*
private BigDecimal customField1;
#Column(name = "ESTIMATED_COST_OVERRUNS", nullable = true, precision = 22, scale = 2)
public BigDecimal getCustomField1() {
return this.customField1;
}
public void setCustomField1(BigDecimal customField1) {
this.customField1 = customField1;
}
*/
}
Just to be clear, I cannot map these fields at compile time. They are purely custom and are defined entirely by users. At runtime, I will be able to know what custom fields do exist, so I would be able to loop through them and add them (as I hoped to do with the add column seen above), but I cannot know before deployment. The custom fields are also subject to change any moment.

For Hibernate 5 you should build MetaData via RegistryService, add property, then build SessionFactory via MetaData (Bootstrap native metadata). Something like this:
public SessionFactory buildSessionFactory(LocalSessionFactoryBuilder sessionFactoryBuilder) {
StandardServiceRegistryBuilder registryBuilder = new StandardServiceRegistryBuilder();
registryBuilder.applySettings(sessionFactoryBuilder.getProperties());
Metadata metaData = getMetadataSources().buildMetadata(registryBuilder.build());
PersistentClass gridMapping = metaData.getEntityBinding(AgreementsGrid.class.getName());
Column column = new Column();
...
Property prop = new Property();
...
gridMapping.addProperty(prop);
SessionFactory sessionFactory = metaData.buildSessionFactory();
return sessionFactory;
}

Related

Update Specific Fields with Spring Data Rest and MongoDB

I'm using Spring Data MongoDB and Spring Data Rest to create a REST API which allows GET, POST, PUT and DELETE operations on my MongoDB database and it's all working fine except for the update operations (PUT). It only works if I send the full object in the request body.
For example I have the following entity:
#Document
public class User {
#Id
private String id;
private String email;
private String lastName;
private String firstName;
private String password;
...
}
To update the lastName field, I have to send all of the user object, including the password ! which is obviously very wrong.
If I only send the field to update, all the others are set to null in my database. I even tried to add a #NotNull constraints on those fields and now the update won't even happens unless I send all of the user object's fields.
I tried searching for a solution here but I only found the following post but with no solution: How to update particular field in mongo db by using MongoRepository Interface?
Is there a way to implement this ?
Spring Data Rest uses Spring Data repositories to automatically retrieve and manipulate persistent data using Rest calls (check out https://docs.spring.io/spring-data/rest/docs/current/reference/html/#reference).
When using Spring Data MongoDB, you have the MongoOperations interface which is used as a repository for your Rest endpoints.
However MongoOperations currently does not supports specific fields updates !
PS: It will be awesome if they add this feature like #DynamicUpdate in Spring Data JPA
But this doesn't mean it can be done, here's the workaround I did when I had this issue.
Firstly let me explain what we're going to do:
We will create a controller which will override all the PUT operations so that we can implement our own update method.
Inside that update method, we will use MongoTemplate which do have the ability to update specific fields.
N.B. We don't want to re-do these steps for each model in our application, so we will retrieve which model to update dynamically. In order to do that we will create a utility class. [This is optional]
Let's start by adding the org.reflections api to our project dependency which allows us to get all the classes which have a specific annotation (#Document in our case):
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.12</version>
</dependency>
Then create a new class, called UpdateUtility and add the following methods and also replace the MODEL_PACKAGE attribute with your own package containing your entities:
public class UpdateUtility {
private static final String MODEL_PACKAGE = "com.mycompany.myproject.models";
private static boolean initialized = false;
private static HashMap<String, Class> classContext = new HashMap<>();
private static void init() {
if(!initialized) {
Reflections reflections = new Reflections(MODEL_PACKAGE);
Set<Class<?>> classes = reflections.getTypesAnnotatedWith(Document.class); // Get all the classes annotated with #Document in the specified package
for(Class<?> model : classes) {
classContext.put(model.getSimpleName().toLowerCase(), model);
}
initialized = true;
}
}
public static Class getClassFromType(String type) throws Exception{
init();
if(classContext.containsKey(type)) {
return classContext.get(type);
}
else {
throw new Exception("Type " + type + " does not exists !");
}
}
}
Using this utility class we can retreive the model class to update from it's type.
E.g: UpdateUtility.getClassFromType() will returns User.class
Now let's create our controller:
public class UpdateController {
#Autowired
private MongoTemplate mongoTemplate;
#PutMapping("/{type}/{id}")
public Object update(#RequestBody HashMap<String, Object> fields,
#PathVariable(name = "type") String type,
#PathVariable(name = "id") String id) {
try {
Class classType = UpdatorUtility.getClassFromType(type); // Get the domain class from the type in the request
Query query = new Query(Criteria.where("id").is(id)); // Update the document with the given ID
Update update = new Update();
// Iterate over the send fields and add them to the update object
Iterator iterator = fields.entrySet().iterator();
while(iterator.hasNext()) {
HashMap.Entry entry = (HashMap.Entry) iterator.next();
String key = (String) entry.getKey();
Object value = entry.getValue();
update.set(key, value);
}
mongoTemplate.updateFirst(query, update, classType); // Do the update
return mongoTemplate.findById(id, classType); // Return the updated document
} catch (Exception e) {
// Handle your exception
}
}
}
Now we're able to update the specified fields without changing the calls.
So in your case, the call would be:
PUT http://MY-DOMAIN/user/MY-USER-ID { lastName: "My new last name" }
PS: You can improve it by adding the possibility to update specific field in a nested objects...

Spring Data Neo4j not mapping Class fields to node properties

I do have a Repository
#Repository
public interface PointOfInterestRepository extends GraphRepository<Poi> {
// currently empty
}
with no custom methods defined. So I use the like of save(T... entities) which are predefined.
And I have my Poi class as follows
#NodeEntity(label = "PointOfInterest")
public class Poi {
#JsonIgnore
#GraphId
Long neo4jId;
#JsonManagedReference("node-poi")
#JsonProperty("node")
#Relationship(type = "BELONGS_TO", direction = Relationship.UNDIRECTED)
private Node node;
#JsonProperty("id")
#Property(name = "poiID")
private final String id;
#JsonProperty("uris")
#Property(name = "uris")
private final Set<URI> correspondingURIs = new HashSet<>();
/* Some more stuff I skip here*/
}
with getters for the fields.
Currently I am able to save such Pois to neo4j and retrieve them back, but when I try to work with those Nodes in the database via cypher it appears that the fields aren't mapped to neo4j properties.
I thought spring-data-neo4j would convert my class fields to neo4j graph properties. Am I wrong with that?
Note: The save calls seems to work very well. After that I can see the Nodes in the database and calling findAll() afterwards will return me all the saved Nodes (Pois) properly with all the correct values. But somehow, within the database, I cannot see any properties/fields.
The problem is the final fields. SDN would be unable to write values back to the entity when loaded from the graph because these fields are final (and SDN will use only the default no-args constructor), and as such, final fields are not supported.
Removing the final should fix this.

Lazy Loading using MyBatis 3 with Java

I am using Mybatis (3.2.7 version) as an ORM framework for my JAVA project.
As I'm from JPA background, I was keen to explore the LAZYLOADING supported by Mybatis.
But I couldn't get across anything substantial.
(I am configuring MYBATIS using JAVA API and annotations solely for querying purpose)
As per the Mybatis documentation:
1. lazyLoadingEnabled: default value=TRUE
Globally enables or disables lazy loading. When enabled, all relations will be lazily
loaded. This value can be superseded for an specific relation by using the fetchType attribute
on it.
2. aggressiveLazyLoading : default value=TRUE
When enabled, an object with lazy loaded properties will be loaded entirely upon a call to any of the lazy properties. Otherwise, each property is loaded on demand.
Using the following attributes, I tried the following code:
a. JAVA Classes :
Feedback.java
public class Feedback implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String message;
/**
* while loading Feedback, I want sender object to be lazily loaded
*/
private User sender;
private boolean seen;
// getters and setters
}
User.java
public class User implements Serializable, {
private static final long serialVersionUID = 1L;
private int id;
private String email;
// getters and setters
}
b. DB schema:
Feedback table
Table "public.feedback"
Column | Type | Modifiers
-------------+-----------+-------------------------------------------------------
id | integer | PRIMARY KEY
seen | boolean | not null
sender_id | integer | FOREIGN KEY (sender_id) REFERENCES users(id)
message | text |
User Table:
Table "public.users"
Column | Type | Modifiers
-------------+----------+----------------------------------------------------
id | integer | PRIMARY KEY
email | text |
c. Configuring MyBatis via JAVA API:
DataSource dataSource = new PGSimpleDataSource();
((PGSimpleDataSource) dataSource).setServerName("localhost");
((PGSimpleDataSource) dataSource).setDatabaseName(dbName);
((PGSimpleDataSource) dataSource).setPortNumber(5432);
((PGSimpleDataSource) dataSource).setUser(new UnixSystem().getUsername());
((PGSimpleDataSource) dataSource).setPassword("");
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment(dbName, transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(FeedbackMapper.class);
//
configuration.setAggressiveLazyLoading(false);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
d. Querying DB and DB Queries in Feedbackmapper:
d.1 Code in Feedbackmapper:
#Select("SELECT f.id, f.message, f.seen, f.sender_id FROM feedback f WHERE f.id= #{feedbackId}")
#Results(value = {
#Result(property = "id", column = "id"),
#Result(property = "sender", column = "sender_id", javaType = User.class, one = #One(select = "getUser", fetchType=FetchType.DEFAULT))
})
public Feedback getFeedback(#Param("feedbackId") int feedbackId);
#Select("SELECT id, email FROM users WHERE id=#{id}")
public User getUser(int id);
d.2: Code to invoke the queries in feedbackMapper
// setup Mybatis session factory and config
Feedback feedback =feedbackMapper.getFeedback(70000);
System.out.println(feedback);
But still the "sender" object is populated upon querying the getFeedback(id). I expect the sender object shouldn't be populated immediately but only when I call getSender() on the fetched feedback object . Please help.
My recent Observations:
Mybatis team has indeed got it wrong in their documentation ie in documentation:
lazyLoadingEnabled: default value=TRUE
aggressiveLazyLoading : default value=TRUE
But looking at their source code:
protected boolean lazyLoadingEnabled = false;
protected boolean aggressiveLazyLoading = true;
**However that being corrected, the results are not affected and lazy loading isnt working :( **
I think I found a way to enable the lazyloading (though not cent-percent sure):
MyBatis documentation has following setting in configuration:
Setting : lazyLoadTriggerMethods
Description : Specifies which Object's methods trigger a lazy load
Valid Values : A method name list separated by commas
Default: equals,clone,hashCode,toString
As per the source code, this thing maps properly to what is given in documentation:
public class Configuration {
// other attributes + methods
protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new
String[] { "equals", "clone", "hashCode", "toString" }));
}
I have changed the Mybatis configuration to follows:
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment(dbName, transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
/**
*This is the cause why LAZY LOADING is working now
*/
configuration.getLazyLoadTriggerMethods().clear();
///////////////////////////////////////////////////
configuration.setLazyLoadingEnabled(true);
configuration.setAggressiveLazyLoading(false);
The queries in Mapper (mostly unchanged):
#Select("SELECT id, message, seen, sender_id
FROM feedback WHERE f.id= #{feedbackId}")
#Results(value = {
#Result(property = "id", column = "id"),
#Result(property = "sender", column = "sender_id", javaType = User.class, one = #One(select = "getUser"))
// Set fetchType as DEFAULT or LAZY or don't set at all-- lazy loading takes place
// Set fetchType as EAGER --sender Object is loaded immediately
})
public Feedback getFeedback(#Param("feedbackId") int feedbackId);
#Select("SELECT id, email FROM users WHERE id=#{id}")
public User getUser(int id);
- JAVA Code to invoke mapper
FeedbackMapper mapper = sqlSession.getMapper(FeedbackMapper.class);
Feedback feedback =mapper.getFeedback(69999);
System.out.println("1. Feedback object before sender lazily load: \n"+ feedback);
System.out.println("2. Sender loaded explicitly \n" +feedback.getSender());
System.out.println("3. Feedback object after sender loading \n" + feedback);
Output of the CODE
1. Feedback object before sender lazily load:
{id : 69999, message : message123, sender : null, seen : false}
2. Sender loaded explicitly
{id : 65538 , email: hemant#gmail.com}
3. Feedback object after sender loading:
{id : 69999, message : message123, sender : {id : 65538, email : hemant#gmail.com},
seen : false}
Though this works satisfactorily, upon doing
configuration.getLazyLoadTriggerMethods().clear();
However to lack of documentation in Mybatis, I'm not sure, whether this is associated with any drawbacks as such.
UPDATE
I looked into the source code, and the problem is that the Configuration class does not reflect the doc.
In configuration class, lazy load is disabled by default. This changed in commit f8ddba364092d819f100e0e8f7dec677c777d588, but the doc was not updated to reflect the change.
protected boolean lazyLoadingEnabled = false;
I filled a bug report https://github.com/mybatis/mybatis-3/issues/214.
For now, add configuration.setLazyLoadingEnabled(true) to enable lazy load.
Old answer:
The documentation is incorrect. When aggressiveLazyLoading is true, all lazy properties are loaded after any method call on the object.
So calling feedback.toString() will fetch the Feedback's sender property.
You should set aggressiveLazyLoading to false to achieve what you want.
I think it's not easy to use print to verify the lazy loading in mybatis.
We can use configuration.getLazyLoadTriggerMethods().clear(); to remove the default triggerMethods as the previous answer. But when we print it, or using toString, It still will call getXXX. so It still trigger lazy loading to select more. So we can't debug or print to see the procedure to lazy loading.
I found a way to verify this function.
set log level to debug.
write the follow codes and see the console.
log.info("user :{}", userLazyDepartment);
log.info("user :{}", userLazyDepartment.getDepartment());

Unable to read Inherited class instances with DataNucles JDO

I was unable to read the full inherited class instances as described in following URL
http://www.datanucleus.org/products/datanucleus/jdo/orm/inheritance.html
Following describes the mapping of classes.
#PersistenceCapable(detachable = "true")
#Discriminator(strategy=DiscriminatorStrategy.CLASS_NAME)
#Inheritance(strategy=InheritanceStrategy.NEW_TABLE)
public class IdeaItem {
#PrimaryKey
#Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
#Column(jdbcType = "INTEGER", length = 11)
private long id;
#Column(name="IDEAID")
private Idea idea;
#Column(jdbcType = "INTEGER", length = 11)
private long showOrder;
}
#PersistenceCapable(detachable = "true")
#Inheritance(strategy=InheritanceStrategy.NEW_TABLE)
public class IdeaItemText extends IdeaItem {
#Column(jdbcType = "VARCHAR", length = 500)
private String text;
}
Data saving part working fine. I inserted "IdeaItemText" object and both "IdeaItem" and "IdeaItemText" tables got updated successfully.
Now I need to read Subclasses by putting "IdeaItem" as an Extent. I executed the following code.
Extent items = getPersistenceManager().getExtent(IdeaItem.class,true);
javax.jdo.Query q = getPersistenceManager().newQuery(items);
List data = (List)q.execute();
As in the JDO docs, this should return the whole object graph. But this is not returning any record. When I check the log, I found that it searching for a reacord where Discriminator Value equals to "com.mydomain.IdeaItem" which does not exists. When I removed the Discriminator annotation I got all the records in the table. Even though how I access the sub classes attributes ? Furthermore how I query subclass attributes with the base class Extent ?
So you didn't let the persistence mechanism know about the subclass (whether that is using auto-start mechanism, persistence.xml, calling pm.getExtent on the subclass, or simply instantiating the subclass.class). It can only query classes that it is "aware of"

How to persist a property of type List<String> in JPA?

What is the smartest way to get an entity with a field of type List persisted?
Command.java
package persistlistofstring;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Persistence;
#Entity
public class Command implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
Long id;
#Basic
List<String> arguments = new ArrayList<String>();
public static void main(String[] args) {
Command command = new Command();
EntityManager em = Persistence
.createEntityManagerFactory("pu")
.createEntityManager();
em.getTransaction().begin();
em.persist(command);
em.getTransaction().commit();
em.close();
System.out.println("Persisted with id=" + command.id);
}
}
This code produces:
> Exception in thread "main" javax.persistence.PersistenceException: No Persistence provider for EntityManager named pu: Provider named oracle.toplink.essentials.PersistenceProvider threw unexpected exception at create EntityManagerFactory:
> oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
> Local Exception Stack:
> Exception [TOPLINK-30005] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
> Exception Description: An exception was thrown while searching for persistence archives with ClassLoader: sun.misc.Launcher$AppClassLoader#11b86e7
> Internal Exception: javax.persistence.PersistenceException: Exception [TOPLINK-28018] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.EntityManagerSetupException
> Exception Description: predeploy for PersistenceUnit [pu] failed.
> Internal Exception: Exception [TOPLINK-7155] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.ValidationException
> Exception Description: The type [interface java.util.List] for the attribute [arguments] on the entity class [class persistlistofstring.Command] is not a valid type for a serialized mapping. The attribute type must implement the Serializable interface.
> at oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException.exceptionSearchingForPersistenceResources(PersistenceUnitLoadingException.java:143)
> at oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider.createEntityManagerFactory(EntityManagerFactoryProvider.java:169)
> at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:110)
> at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:83)
> at persistlistofstring.Command.main(Command.java:30)
> Caused by:
> ...
Use some JPA 2 implementation: it adds a #ElementCollection annotation, similar to the Hibernate one, that does exactly what you need. There's one example here.
Edit
As mentioned in the comments below, the correct JPA 2 implementation is
javax.persistence.ElementCollection
#ElementCollection
Map<Key, Value> collection;
See: http://docs.oracle.com/javaee/6/api/javax/persistence/ElementCollection.html
Should anyone be looking for an alternative solution where you store your string lists as one field in your database, here's how I solved that. Create a Converter like this:
import java.util.Arrays;
import java.util.List;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import static java.util.Collections.*;
#Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {
private static final String SPLIT_CHAR = ";";
#Override
public String convertToDatabaseColumn(List<String> stringList) {
return stringList != null ? String.join(SPLIT_CHAR, stringList) : "";
}
#Override
public List<String> convertToEntityAttribute(String string) {
return string != null ? Arrays.asList(string.split(SPLIT_CHAR)) : emptyList();
}
}
Now use it on your Entities like this:
#Convert(converter = StringListConverter.class)
private List<String> yourList;
In the database, your list will be stored as foo;bar;foobar, and in your Java object you will get a list with those strings.
It seems none of the answers explored the most important settings for an #ElementCollection mapping.
When you map a list with this annotation and let JPA/Hibernate auto-generate the tables, columns, etc., it'll use auto-generated names as well.
So, let's analyze a basic example:
#Entity
#Table(name = "sample")
public class MySample {
#Id
#GeneratedValue
private Long id;
#ElementCollection // 1
#CollectionTable(name = "my_list", joinColumns = #JoinColumn(name = "id")) // 2
#Column(name = "list") // 3
private List<String> list;
}
The basic #ElementCollection annotation (where you can define the known fetch and targetClass preferences)
The #CollectionTable annotation is very useful when it comes to giving a name to the table that'll be generated, as well as definitions like joinColumns, foreignKey's, indexes, uniqueConstraints, etc.
#Column is important to define the name of the column that'll store the varchar value of the list.
The generated DDL would be:
-- table sample
CREATE TABLE sample (
id bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id)
);
-- table my_list
CREATE TABLE IF NOT EXISTS my_list (
id bigint(20) NOT NULL,
list varchar(255) DEFAULT NULL,
FOREIGN KEY (id) REFERENCES sample (id)
);
This answer was made pre-JPA2 implementations, if you're using JPA2, see the ElementCollection answer above:
Lists of objects inside a model object are generally considered "OneToMany" relationships with another object. However, a String is not (by itself) an allowable client of a One-to-Many relationship, as it doesn't have an ID.
So, you should convert your list of Strings to a list of Argument-class JPA objects containing an ID and a String. You could potentially use the String as the ID, which would save a little space in your table both from removing the ID field and by consolidating rows where the Strings are equal, but you would lose the ability to order the arguments back into their original order (as you didn't store any ordering information).
Alternatively, you could convert your list to #Transient and add another field (argStorage) to your class that is either a VARCHAR() or a CLOB. You'll then need to add 3 functions: 2 of them are the same and should convert your list of Strings into a single String (in argStorage) delimited in a fashion that you can easily separate them. Annotate these two functions (that each do the same thing) with #PrePersist and #PreUpdate. Finally, add the third function that splits the argStorage into the list of Strings again and annotate it #PostLoad. This will keep your CLOB updated with the strings whenever you go to store the Command, and keep the argStorage field updated before you store it to the DB.
I still suggest doing the first case. It's good practice for real relationships later.
We can also use this.
#Column(name="arguments")
#ElementCollection(targetClass=String.class)
private List<String> arguments;
According to Java Persistence with Hibernate
mapping collections of value types with annotations [...]. At the time of writing it isn't part of the Java Persistence standard
If you were using Hibernate, you could do something like:
#CollectionOfElements(targetElement = String.class)
#JoinTable(name = "foo", joinColumns = #JoinColumn(name = "foo_id"))
#IndexColumn(name = "POSITION", base = 1)
#Column(name = "baz", nullable = false)
private List<String> arguments = new ArrayList<String>();
Update: Note, this is now available in JPA2.
When using the Hibernate implementation of JPA , I've found that simply declaring the type as an ArrayList instead of List allows hibernate to store the list of data.
Clearly this has a number of disadvantages compared to creating a list of Entity objects. No lazy loading, no ability to reference the entities in the list from other objects, perhaps more difficulty in constructing database queries. However when you are dealing with lists of fairly primitive types that you will always want to eagerly fetch along with the entity, then this approach seems fine to me.
#Entity
public class Command implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
Long id;
ArrayList<String> arguments = new ArrayList<String>();
}
I had the same problem so I invested the possible solution given but at the end I decided to implement my ';' separated list of String.
so I have
// a ; separated list of arguments
String arguments;
public List<String> getArguments() {
return Arrays.asList(arguments.split(";"));
}
This way the list is easily readable/editable in the database table;
Ok i know its bit late. But for those brave souls that will see this as time passes.
As written in documentation:
#Basic:
The simplest type of mapping to a database column. The Basic annotation can be applied to a persistent property or instance variable of any of the following types: Java primitive types, [...], enums, and any other type that implements java.io.Serializable.
The important part is type that implements Serializable
So by far the most simple and easiest to use solution is simply using ArrayList instead of List (or any serializable container):
#Basic
ArrayList<Color> lovedColors;
#Basic
ArrayList<String> catNames;
Remember however that this will use system serialization, so it will come with some price, such as:
if serialized object model will change, u might not be able to restore data
small overhead is added for each element stored.
In short
it is quite simple to store flags or few elements, but i would not
recomend it to store data that might grow big.
Here is the solution for storing a Set using #Converter and StringTokenizer. A bit more checks against #jonck-van-der-kogel solution.
In your Entity class:
#Convert(converter = StringSetConverter.class)
#Column
private Set<String> washSaleTickers;
StringSetConverter:
package com.model.domain.converters;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
#Converter
public class StringSetConverter implements AttributeConverter<Set<String>, String> {
private final String GROUP_DELIMITER = "=IWILLNEVERHAPPEN=";
#Override
public String convertToDatabaseColumn(Set<String> stringList) {
if (stringList == null) {
return new String();
}
return String.join(GROUP_DELIMITER, stringList);
}
#Override
public Set<String> convertToEntityAttribute(String string) {
Set<String> resultingSet = new HashSet<>();
StringTokenizer st = new StringTokenizer(string, GROUP_DELIMITER);
while (st.hasMoreTokens())
resultingSet.add(st.nextToken());
return resultingSet;
}
}
Thiago answer is correct, adding sample more specific to question, #ElementCollection will create new table in your database, but without mapping two tables, It means that the collection is not a collection of entities, but a collection of simple types (Strings, etc.) or a collection of embeddable elements (class annotated with #Embeddable).
Here is the sample to persist list of String
#ElementCollection
private Collection<String> options = new ArrayList<String>();
Here is the sample to persist list of Custom object
#Embedded
#ElementCollection
private Collection<Car> carList = new ArrayList<Car>();
For this case we need to make class Embeddable
#Embeddable
public class Car {
}
As my reputation is not enough yet to comment on the much underrated answer written by #razvang:
As this question was asked over a decade ago, keep in mind much of the world has changed in the time since. We now have databases with native JSON column support and can use this functionality instead of using separate entities, joins or custom String-to-List converters, which are used by the other answers.
Let me suggest two purely optional changes to #razvang's superb answer though, which might be interesting depending on your specific situation:
You could omit the auto_apply = true and add #Convert(converter = <CONVERTER_CLASS_NAME>.class) to the entity field to keep control over when your converter is used.
Instead of throwing a RuntimeException whenever a conversion fails, you could handle the error right there (for example pass an empty list and write a log message) to make it fail somewhat gracefully.
What I wanted was a simple way of persisting a set of Strings, in a table column.
I ended up using JSON, as MySQL 5.7+, has native support.
Here's my solution
#Column(name = "eligible_approvers", columnDefinition = "json")
#Convert(converter = ArrayJsonConverter.class)
private Set<String> eligibleApprovers;
And then write a very basic converter
#Converter(autoApply = true)
public class ArrayJsonConverter implements AttributeConverter<Set, String> {
static final ObjectMapper mapper = new ObjectMapper();
#Override
public String convertToDatabaseColumn(Set list) {
if (list == null)
return null;
try {
return mapper.writeValueAsString(list);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
#Override
public Set convertToEntityAttribute(String dbJson) {
if (dbJson == null)
return null;
try {
return mapper.readValue(dbJson, new TypeReference<Set<String>>() {
});
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
My fix for this issue was to separate the primary key with the foreign key. If you are using eclipse and made the above changes please remember to refresh the database explorer. Then recreate the entities from the tables.

Categories