Документ не сохраняется в приложении spring jpa document manager

Я разрабатываю приложение для управления документами в spring с помощью jpa и MySQL. Приложение в настоящее время принимает документ и его метаданные из пользовательской веб-формы createOrUpdateDocumentForm.jsp в контроллер DocumentController.java. Однако данные не попадают в базу данных MySQL. Может ли кто-нибудь показать мне, как изменить мой код, чтобы документ и его метаданные были сохранены в базовой базе данных?

Поток данных (включая документ pdf), по-видимому, проходит через следующие объекты:

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  

Я обобщу соответствующие части каждого из этих объектов следующим образом:

jsp запускает следующий метод в 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}";
    }
}

Когда я отправляю документ через веб-форму в jsp в controller, команды System.out.println() в коде controller выводят следующее, что указывает на то, что данные фактически отправляются на сервер:

----------- 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: [[email protected] 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

Модель Document.java:

@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);}

}

Код ClinicService.java, который вызывается из DocumentController:

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);}

Соответствующий код в JpaDocumentRepository.java:

@PersistenceContext
private EntityManager em;

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

Наконец, соответствующие части кода SQL, которые создают базу данных, включают:

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)
);  

Какие изменения я внес в этот код, чтобы он сохранял document в таблице documents базы данных MySQL, используя jpa?

Ответ 1

Ваши сопоставления JPA кажутся хорошими. Очевидно, что @Lob требует, чтобы тип данных был байтом []/Byte []/или java.sql.Blob. Исходя из этого, плюс ваши симптомы и отладочная распечатка, кажется, ваш код делает правильные манипуляции с данными (примечания JPA хороши), но комбинация spring + MySQL не выполняется. Это предполагает небольшую проблему с вашей транзакционной конфигурацией spring OR с типом данных MySQL.

1. Транзакционное поведение

Соответствующий код в JpaDocumentRepository.java:

@PersistenceContext
private EntityManager em;

@Override
public void save(Document document) {
    if (document.getId() == null) {this.em.persist(document);}
    else {this.em.merge(document);}
}  
  • Вы не используете EJB (следовательно, нет "автоматических" транзакций, управляемых контейнером).
  • Вы используете JPA в классах Servlets/java (следовательно, вам требуется "ручная" демаркация транзакций - внешний контейнер сервлетов, в вашем коде или через конфигурацию spring).
  • Вы вводите менеджера сущностей через @PersistenceContext (т.е. управляемый сущностью контейнерный менеджер, поддерживаемый JTA, а не локальная транзакция ресурса Entity Manager, em.getTransaction())
  • Вы отметили свой "родительский" метод как @Transactional (т.е. spring проприетарные трансконты - аннотация, стандартизованная позже в Java EE 7).

Аннотации и код должны давать транзакционное поведение. У вас есть spring правильно настроенный для транзакций JTA? (Использование JtaTransactionManager, а не DataSourceTransactionManager, который дает локальные транзакции драйвера JDBC) spring XML должен содержать нечто очень похожее на:

<!-- 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>

Следите за дополнительными параметрами/настройками.

Это закодированная вручную версия того, что должен делать spring (только для понимания - не кодируйте это). Использует UserTransaction (JTA), а не em.getTransaction() типа EntityTransaction (локальный JDBC):

// 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

Как показано здесь (внизу), MySql Blobs немного отличается от других баз данных. Различные Blobs и их максимальная емкость:

TINYBLOB - 255 байт BLOB - 65535 байт MEDIUMBLOB - 16,777,215 байт (2 ^ 24 - 1) LONGBLOB - 4G байт (2 ^ 32 - 1)

Если (2) окажется вашей проблемой:

  • увеличить тип MySQL до MEDIUMBLOB или LONGBLOB
  • исследуйте, почему вы не видели сообщение об ошибке (v важно). Правильно ли настроен ваш журнал? Вы проверяли журналы?

Ответ 2

@CodeMed, мне потребовалось некоторое время, но я смог воспроизвести проблему. Это может быть проблема конфигурации: @PersistenceContext может быть отсканирован дважды, он может быть проверен вашим корневым контекстом и вашим веб-контекстом. Это приведет к тому, что @PersistenceContext будет использоваться совместно, поэтому он не сохраняет ваши данные (Spring не позволяет этого). Мне показалось странным, что нет сообщений или журналов, где отображается. если вы попробовали этот фрагмент ниже на вашем "Сохранить" (документ документа), вы увидите фактическую ошибку:

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

Чтобы решить проблему, вы можете сделать следующее (избегайте проверки @PersistenceContext дважды):

1- Убедитесь, что весь ваш контроллер находится в отдельном пакете, например com.mycompany.myapp.controller, а в вашем веб-контексте используйте компонент-сканирование как <context:component-scan annotation-config="true" base-package="com.mycompany.myapp.controller" />

2- Убедитесь, что другие компоненты находятся в другом пакете, отличном от пакета контроллера, например: com.mycompany.myapp.dao, com.mycompany.myapp.service.... а затем в корневом контексте используйте компонент-сканирование как <context:component-scan annotation-config="true" base-package="com.mycompany.myapp.service, com.mycompany.myapp.dao" />

Или покажите мне свои spring xml-конфигурации и ваш web.xml, я укажу вам в правильном направлении

Ответ 3

Я не эксперт по Hibernate-with-annotations (я использую его с 2004 года, но с конфигурацией XML). Во всяком случае, я думаю, что вы неправильно смешиваете аннотации. Вы указали, что не хотите, чтобы поле file сохранялось с @Transient, но вы также сказали, что это @Lob, что означает, что вы хотите, чтобы он сохранялся. Похоже, что @Lob выигрывает, а Hibernate пытается разрешить поле в столбце, используя имя поля.

Снимите @Lob, и я думаю, что вы будете установлены.

Ответ 4

создал образец приложения, которое будет принимать файл и хранить его в базе данных mysql, малое хранилище данных, так как использование blob/clob - очень старый способ делать. Теперь все базы данных обновлены для хранения в формате массива байтов, этот пример может помочь к вашему требованию.

используемые технологии: spring 3.2 + hibernate 4.x

github Ссылка: https://github.com/uttesh/SpringHibernateUploader

Ответ 5

Это не прямой ответ на ваш вопрос (извините, но я не поклонник спящего режима, поэтому не могу вам помочь), но вы должны рассмотреть возможность использования базы данных NoSQL, такой как MongoDB, а не MySQL для работы вроде это. Я пробовал оба, и базы данных NoSQL намного лучше подходят для такого рода требований.

Вы обнаружите, что в таких ситуациях он работает намного лучше, чем может сделать MySQL, и SpringData MongoDB позволяет вам очень легко сохранять и загружать объекты Java, которые автоматически сопоставляются с MongoDB.