12

I am developing a document management application in spring using jpa and MySQL. The application is currently accepting a document and its meta data from a user web form createOrUpdateDocumentForm.jsp into the controller DocumentController.java. However, the data is not making its way into the MySQL database. Can someone show me how to alter my code so that the document and its metadata get stored in the underlying database?

The flow of data (including the pdf document) seems to go through the following objects:

createOrUpdateDocumentForm.jsp  //omitted for brevity, since it is sending data to controller (see below)
Document.java  
DocumentController.java  
ClinicService.java
JpaDocumentRepository.java
The MySQL database  

I will summarize relevant parts of each of these objects as follows:

The jsp triggers the following method in DocumentController.java:

@RequestMapping(value = "/patients/{patientId}/documents/new", headers = "content-type=multipart/*", method = RequestMethod.POST)
public String processCreationForm(@ModelAttribute("document") Document document, BindingResult result, SessionStatus status, @RequestParam("file") final MultipartFile file) {
    document.setCreated();
    byte[] contents;
    Blob blob = null;
    try {
        contents = file.getBytes();
        blob = new SerialBlob(contents);
    } catch (IOException e) {e.printStackTrace();}
    catch (SerialException e) {e.printStackTrace();}
    catch (SQLException e) {e.printStackTrace();}
    document.setContent(blob);
    document.setContentType(file.getContentType());
    document.setFileName(file.getOriginalFilename());
    System.out.println("----------- document.getContentType() is: "+document.getContentType());
    System.out.println("----------- document.getCreated() is: "+document.getCreated());
    System.out.println("----------- document.getDescription() is: "+document.getDescription());
    System.out.println("----------- document.getFileName() is: "+document.getFileName());
    System.out.println("----------- document.getId() is: "+document.getId());
    System.out.println("----------- document.getName() is: "+document.getName());
    System.out.println("----------- document.getPatient() is: "+document.getPatient());
    System.out.println("----------- document.getType() is: "+document.getType());        
    try {System.out.println("[[[[BLOB LENGTH IS: "+document.getContent().length()+"]]]]");}
    catch (SQLException e) {e.printStackTrace();}
    new DocumentValidator().validate(document, result);
    if (result.hasErrors()) {
        System.out.println("result.getFieldErrors() is: "+result.getFieldErrors());
        return "documents/createOrUpdateDocumentForm";
    }
    else {
        this.clinicService.saveDocument(document);
        status.setComplete();
        return "redirect:/patients?patientID={patientId}";
    }
}

When I submit a document through the web form in the jsp to the controller, the System.out.println() commands in the controller code output the following, which indicate that the data is in fact getting sent to the server:

----------- document.getContentType() is: application/pdf
----------- document.getCreated() is: 2013-12-16
----------- document.getDescription() is: paper
----------- document.getFileName() is: apaper.pdf
----------- document.getId() is: null
----------- document.getName() is: apaper
----------- document.getPatient() is: [Patient@564434f7 id = 1, new = false, lastName = 'Frank', firstName = 'George', middleinitial = 'B', sex = 'Male', dateofbirth = 2000-11-28T16:00:00.000-08:00, race = 'caucasian']
----------- document.getType() is: ScannedPatientForms
[[[[BLOB LENGTH IS: 712238]]]]  //This indicates the file content was converted to blob

The Document.java model is:

@Entity
@Table(name = "documents")
public class Document {
    @Id
    @GeneratedValue
    @Column(name="id")
    private Integer id;

    @ManyToOne
    @JoinColumn(name = "client_id")
    private Patient patient;

    @ManyToOne
    @JoinColumn(name = "type_id")
    private DocumentType type;

    @Column(name="name")
    private String name;

    @Column(name="description")
    private String description;

    @Column(name="filename")
    private String filename;

    @Column(name="content")
    @Lob
    private Blob content;

    @Column(name="content_type")
    private String contentType;

    @Column(name = "created")
    private Date created;

    public Integer getId(){return id;}
    public void setId(Integer i){id=i;}

    protected void setPatient(Patient patient) {this.patient = patient;}
    public Patient getPatient(){return this.patient;}

    public void setType(DocumentType type) {this.type = type;}
    public DocumentType getType() {return this.type;}

    public String getName(){return name;}
    public void setName(String nm){name=nm;}

    public String getDescription(){return description;}
    public void setDescription(String desc){description=desc;}

    public String getFileName(){return filename;}
    public void setFileName(String fn){filename=fn;}

    public Blob getContent(){return content;}
    public void setContent(Blob ct){content=ct;}

    public String getContentType(){return contentType;}
    public void setContentType(String ctype){contentType=ctype;}

    public void setCreated(){created=new java.sql.Date(System.currentTimeMillis());}
    public Date getCreated() {return this.created;}

    @Override
    public String toString() {return this.getName();}
    public boolean isNew() {return (this.id == null);}

}

The ClinicService.java code that is called from the DocumentController is:

private DocumentRepository documentRepository;
private PatientRepository patientRepository;

@Autowired
public ClinicServiceImpl(DocumentRepository documentRepository, PatientRepository patientRepository) {
    this.documentRepository = documentRepository;
    this.patientRepository = patientRepository;
}

@Override
@Transactional
public void saveDocument(Document doc) throws DataAccessException {documentRepository.save(doc);}

The relevant code in JpaDocumentRepository.java is:

@PersistenceContext
private EntityManager em;

@Override
public void save(Document document) {
    if (document.getId() == null) {this.em.persist(document);}
    else {this.em.merge(document);}
}  

Finally, the relevant parts of the SQL code that creates the database include:

CREATE TABLE IF NOT EXISTS documenttypes (
  id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(80),
  INDEX(name)
);

CREATE TABLE IF NOT EXISTS patients (
  id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  first_name VARCHAR(30),
  middle_initial VARCHAR(5), 
  last_name VARCHAR(30),
  sex VARCHAR(20), 
  date_of_birth DATE,
  race VARCHAR(30), 
  INDEX(last_name)
);

CREATE TABLE IF NOT EXISTS documents (
  id int(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  client_id int(4) UNSIGNED NOT NULL,
  type_id INT(4) UNSIGNED, 
  name varchar(200) NOT NULL,
  description text NOT NULL,
  filename varchar(200) NOT NULL,
  content mediumblob NOT NULL, 
  content_type varchar(255) NOT NULL,
  created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (client_id) REFERENCES patients(id),
  FOREIGN KEY (type_id) REFERENCES documenttypes(id)
);  

What changes do I make to this code so that it saves the document in the documents table of the MySQL database using jpa?

CodeMed
  • 9,527
  • 70
  • 212
  • 364
  • I assume that there are no errors and that other data are committed to the DB? – Scary Wombat Dec 17 '13 at 01:24
  • @user2310289 I have not seen errors in eclipse console during this operation. Another module in this application does commit data to add a person to the DB, but the add document module does not commit any data. The only error comes during loading of the app in tomcat server, but the app does load and then the code in the current question runs without throwing any error. If you think the error that happens during loading of the application is relevant, you can look at it in this posting: http://stackoverflow.com/questions/20622746/exception-loading-sessions-from-persistent-storage – CodeMed Dec 17 '13 at 01:32
  • "The only error comes during loading of the app in tomcat server" Wich is? Also, are your form on jsp with this attribute:`enctype="multipart/form-data"` – Jorge Campos Dec 17 '13 at 05:19
  • Before trying to give an answer I would like to ask you to make an `em.flush()` and tell us whether that works. – V G Dec 17 '13 at 11:15
  • Also could you please just also specify what JPA provider you are using, Hibernate? In the meantime, as an assumption, I'd like to suggest you to try such `@Lob` combination: instead of `Blob` type you can use `byte[]` type. And if you don't want this to be loaded, you can add annotation `@Basic(fetch = FetchType.LAZY)`. So everything would look like:`@Column(name="content") @Lob @Basic(fetch = FetchType.LAZY) private byte[] content;` – n1ckolas Dec 17 '13 at 12:19
  • did you try to throw Exception in save method and post the trace like ( new RuntimeException().printStrackTrace();) . and check the transaction invoked or not (org.springframework.transaction.interceptor.TransactionInterceptor.invoke) like that. ( I used to do this trick to know what is happening) – Mani Dec 19 '13 at 05:03

4 Answers4

5

@CodeMed, it took me a while, but I was able to reproduce the issue. It might be a configuration issue : @PersistenceContext might be scanned twice, it might be scanned by your root-context and your web-context. This cause the @PersistenceContext to be shared, therefore it is not saving your data (Spring doesn't allow that). I found it weird that no messages or logs where displayed . if you tried this snippet below on you Save(Document document) you will see the actual error :

Session session = this.em.unwrap(Session.class);
session.persist(document);

To solve the problem, you can do the following (avoid the @PersistenceContext to be scanned twice) :

1- Make sure that all your controller are in a separate package like com.mycompany.myapp.controller, and in your web-context use the component-scan as <context:component-scan annotation-config="true" base-package="com.mycompany.myapp.controller" />

2- Make sure that others component are in differents package other than the controller package , for example : com.mycompany.myapp.dao, com.mycompany.myapp.service .... and then in your root-context use the component-scan as <context:component-scan annotation-config="true" base-package="com.mycompany.myapp.service, com.mycompany.myapp.dao" />

Or show me yours spring xml configurations and your web.xml, I will point you to the right direction

storm_buster
  • 7,362
  • 18
  • 53
  • 75
  • To make it clear, The document file (Lob) itself is not the problem. I removed every attributes except the Id and the name, I still wasn't able to save it. The configuration change made it work. I added back the attributes, all get saved without any issue. – storm_buster Dec 17 '13 at 11:27
  • +1 thank you. someone else wrote a whole app and uploaded it to github. I would have divided the bounty a few ways, but S.O. did not give me the interface with which to do that. Looks like you got 4 upvotes so far though. Thank you again. – CodeMed Dec 23 '13 at 20:58
3

I'm not a Hibernate-with-annotations expert (I've been using it since 2004, but with XML config). Anyway, I'm thinking that you're mixing annotations incorrectly. You've indicated that you don't want the file field persisted with @Transient, but you've also said it's a @Lob, which implies you do want it persisted. Looks like @Lob is winning, and Hibernate is trying to resolve the field to a column by using the field name.

Take off the @Lob and I think you'll be set.

MikeThomas
  • 544
  • 3
  • 6
  • +1 Thank you for pointing out a possible solution. I tried your suggestion, and it exposed another error in business-config.xml. I added links to the stack trace and the business-config.xml as an edit to my original posting above. Can you please look into it to find the error? I cannot tell if your answer fixes my problem until I get the application running. – CodeMed Dec 15 '13 at 01:06
  • I made it work without that, the error was with the transaction. It was shared. – storm_buster Dec 17 '13 at 04:52
3

Your JPA mappings seem good. Obviously, @Lob requires data type to be byte[] / Byte[] / or java.sql.Blob. Based on that, plus your symptoms and debugging printout it seems your code doing the correct data manipulation (JPA annotations good), but the combination of spring + MySQL isn't commiting. This suggests a minor problem with your spring transactional config OR with your MySQL data type.

1. Transactional Behaviour

The relevant code in JpaDocumentRepository.java is:

@PersistenceContext
private EntityManager em;

@Override
public void save(Document document) {
    if (document.getId() == null) {this.em.persist(document);}
    else {this.em.merge(document);}
}  
  • You're not using EJBs (hence no 'automatic' container-managed transactions).
  • You're using JPA within Servlets/java classes (hence you require 'manual' transaction demarcation - outside servlet container; in your code or via Spring config).
  • You are injecting the entity manager via @PersistenceContext (i.e. container-managed entity manager backed by JTA, not a Entity Manager resource-local transaction, em.getTransaction())
  • You have marked your 'parent' method as @Transactional (i.e. spring proprietary transcations - annotation later standardised in Java EE 7).

The annotations and code should give transactional behaviour. Do you have a Spring correctly configured for JTA transactions? (Using JtaTransactionManager, not DataSourceTransactionManager which gives JDBC driver local transactions) Spring XML should contain something very similar to:

<!-- JTA requires a container-managed datasource -->
<jee:jndi-lookup id="jeedataSource" jndi-name="jdbc/mydbname"/> 

<!-- enable the configuration of transactional behavior based on annotations -->
<tx:annotation-driven transaction-manager="txManager"/>

<!-- a PlatformTransactionManager is still required -->
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" >
  <!-- (this dependency "jeedataSource" must be defined somewhere else) -->
  <property name="dataSource" ref="jeedataSource"/>  
</bean>

Be suspicious of additional parameters / settings.

This is the manually coded version of what Spring must do (for understanding only - don't code this). Uses UserTransaction (JTA), not em.getTransaction() of type EntityTransaction (JDBC local):

// inject a reference to the servlet container JTA tx
@Resource UserTransaction jtaTx;

// servlet container-managed EM
@PersistenceContext private EntityManager em; 

public void save(Document document) {
    try {
        jtaTx.begin();
        try {
            if (document.getId() == null) {this.em.persist(document);}
            else {this.em.merge(document);}
            jtaTx.commit();
        } catch (Exception e) {
             jtaTx.rollback();
             // do some error reporting / throw exception ...
        }
    } catch (Exception e) {
        // system error - handle exceptions from UserTransaction methods
        // ...
    }
}

2. MySQL Data Type

As shown here (at bottom), MySql Blobs are a bit special compared to other databases. The various Blobs and their maximum storage capacities are:

TINYBLOB - 255 bytes BLOB - 65535 bytes MEDIUMBLOB - 16,777,215 bytes (2^24 - 1) LONGBLOB - 4G bytes (2^32 – 1)

If (2) turns out to be your problem:

  • increase the MySQL type to MEDIUMBLOB or LONGBLOB
  • investigate why you didn't see an error message (v important). Was your logging properly configured? Did you check logs?
Glen Best
  • 22,769
  • 3
  • 58
  • 74
  • Wont work, because he already has `@Transactional` managing his transactions – storm_buster Dec 17 '13 at 04:54
  • Thanks. Didn't see that spring snippet. Have modified answer. – Glen Best Dec 17 '13 at 08:19
  • Thé Lob isn't the problem, if he removes it, he will still have the issue.the snippet is if he tries to get the session from the entityManager and use it to save the object, you will get an error and an exception. – storm_buster Dec 17 '13 at 11:23
  • I agree that there seems to be a transaction management problem - i.e. he's probably running JDBC local transactions, but the EM is managed (backed by JTA) - hence answer part (1). But I 'agree to disagree :)' re LOB being a potential extra problem. A 64kB is fairly small for a PDF - hence answer part (2). – Glen Best Dec 19 '13 at 02:34
  • your part 1 might solve the problem, I didn't try it but, I think he dont need that much configuration, i managed to make it work without all that but just the entityManagerFactory bean. part 2 has nothing the to do with the current issue. He has no saving at all with no error log. if it was a space issue, I am very sure that the code would have thrown an exception – storm_buster Dec 19 '13 at 05:51
  • Resorting to entityManagerFactory bean is switching from a Java EE container-managed EM (backed by JTA) to an application-managed EM (backed by JDBC transactions). It has it's place, but it is less powerful & is divorced from the container. i.e. it's a work-around but not a total solution – Glen Best Dec 19 '13 at 06:00
  • @GlenBest +1 Thank you. The problem was with the setup of the MySQL database. I marked yours as the answer because you mentioned a possibility close to that. – CodeMed Dec 23 '13 at 21:00
1

This is not a direct answer to your question (sorry but I'm not a fan of hibernate so can't really help you there) but you should consider using a NoSQL database such as MongoDB rather than MySQL for a job like this. I've tried both and the NoSQL databases are a much better fit to this sort of requirement.

You will find that in situations like this it performs much better than MySQL can do and SpringData MongoDB allows you to very easily save and load Java objects that automatically get mapped to MongoDB ones.

markdsievers
  • 7,151
  • 11
  • 51
  • 83
Tim B
  • 40,716
  • 16
  • 83
  • 128