Storing Files with Hibernate
Overview
This document uses a couple infrastructure approaches described elsewhere:
There are times, especially in a clustered environment, where you might wish that file content was stored in a database, with full transaction isolation. Or maybe you are just a domain model bigot like myself, and prefer to have any file content operations that are part of your problem domain tied to your entities, and thus find using Hibernate to be a useful way of facilitating this abstraction. Either way, for the curious, here is some guy's approach to storing file content thru his domain model, using Hibernate and MySQL.
A Simple File-Aware Domain Model
First off, let's create a sample domain model to show what we're dealing with. Let's suppose that we want to store OSS Component information in a database that our corporate overlords can browse when they need to make themselves feel like they are appropriately controlling what they perceive as being our cowboy code-slinging ways. The information they want is some description of the OSS Component, also storing the file content of the component's distribution, further allowing the attach ment of any relevent associated legal OSS License Documents (as file content) to the OSS Component Entity.
Create the Hibernate Mappings
To describe these entities as they relate to the database, we might create the following hibernate Entity mappings:
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.techlunch.newb.model.OssComponent" table="OSS_COMPONENTS"> <id name="id" column="OSS_COMPONENT_ID"> <generator class="native" /> </id> <property name="name" column="NAME" not-null="true" /> <property name="desc" column="DESCR" not-null="true" /> <property name="version" column="VERSION" not-null="true" /> <property name="dlUrl" column="DOWNLOAD_URL" not-null="true" /> <property name="created" column="CREATED" not-null="true" /> <property name="fileName" column="FILE_NAME" not-null="true" /> <property name="fileSize" column="FILE_SIZE" not-null="true" /> <property name="contentType" column="CONTENT_TYPE" not-null="true" /> <property name="distro" not-null="true"> <column name="DISTRO_FILE" sql-type="MEDIUMBLOB" /> </property> <set name="licenseDocs" cascade="delete,all-delete-orphan" inverse="true" order-by="FILE_NAME asc"> <key column="OSS_COMPONENT_ID" /> <one-to-many class="com.techlunch.newb.model.OssLicenseDoc" /> </set> </class> </hibernate-mapping>
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.techlunch.newb.model.OssLicenseDoc" table="OSS_LICENSE_DOCS"> <id name="id" column="OSS_LICENSE_DOC_ID"> <generator class="native" /> </id> <property name="fileName" column="FILE_NAME" not-null="true" /> <property name="fileSize" column="FILE_SIZE" not-null="true" /> <property name="contentType" column="CONTENT_TYPE" not-null="true" /> <property name="dlUrl" column="DOWNLOAD_URL" not-null="true" /> <property name="content" not-null="true"> <column name="FILE_CONTENT" sql-type="MEDIUMBLOB" /> </property> <many-to-one name="ossComponent" column="OSS_COMPONENT_ID" not-null="true" /> </class> </hibernate-mapping>
One of the first elements of "strangeness" that you might notice is the use of a contained <column> tag in our file content property, rather than just specifying a column attribute for the property. This is done, because the property tag doesn't specify an attribute that can define a DBMS-native data type, and by default, the blob Hibernate type is mapped by the MySQL dialect to the BLOB MySQL type, which can only store something like 64 KB worth of data. I wanted to be able to store a bit more info, so I wanted to use the MEDIUMBLOB type (something like 16 MB), and this was the only way to specify a native data type.
Create the Entity Classes
My next step was to create my Entity POJOs, but a couple stylistic preferences make the default Hibernate situation look a bit sub-optimal to me. First off, Hibernate maps blob properties to the java.sql.Blob object--not necessarily the funnest property object type to expose directly from your Entity. I didn't want external users of my Entity to necessarily care that the file content was stored as a Blob, or to have to create a Blob if they want to store new file content, so I created my content blob property with a private get/set pair, and only expose a public get/set pair that deals with InputStreams for the file content. The next suboptimal default situation is that Hibernate wants to map your collection associations as a Set. Sets aren't the easiest thing in the world to deal with as properties, especially for a presentation framework like Stripes (for reasons that Tim can and has gone into way better than I can). Also, adding entries to the set by default would be done directly thru the set, which could potentially leave the model in an inconsistent state (by, say, adding the child to the set, but not setting the parent property of the child to the guy holding the set). As such, I keep the mapped association collection property private, expose a read-only List created from the private Set, and define add() and remove() methods on the parent to facilitate parent-child association. This has its own set of complexities associated with it, but as long as you reload your entities every request (rather than keeping them around and re-attaching your entities), you shouldn't run into a problem. The resulting classes look like:
package com.techlunch.newb.model; import java.io.*; import java.sql.Blob; import java.sql.SQLException; import java.util.*; import org.hibernate.*; public class OssComponent { private Long id = null; private String name = null; private String desc = null; private String dlUrl = null; private String version = null; private Blob distro = null; private String fileName = null; private String contentType = null; private Long fileSize = null; private Date created = null; private Set<OssLicenseDoc> licenseDocs = null; public OssComponent() { licenseDocs = new HashSet<OssLicenseDoc>(); } private Blob getDistro() { return distro; } private void setDistro( Blob distro ) { this.distro = distro; } public InputStream getDistroStream() throws SQLException { if (getDistro() == null) return null; return getDistro().getBinaryStream(); } public void setDistroContent( InputStream sourceStream ) throws IOException { setDistro( Hibernate.createBlob( sourceStream ) ); } public String getDlUrl() { return dlUrl; } public void setDlUrl( String dlUrl ) { this.dlUrl = dlUrl; } public Long getId() { return id; } private void setId( Long id ) { this.id = id; } private Set<OssLicenseDoc> getLicenseDocs() { return licenseDocs; } private void setLicenseDocs( Set<OssLicenseDoc> licenseDocs ) { this.licenseDocs = licenseDocs; } public List<OssLicenseDoc> getLicenseDocList() { Set<OssLicenseDoc> set = getLicenseDocs(); List<OssLicenseDoc> lst = new ArrayList<OssLicenseDoc>( set ); return Collections.unmodifiableList( lst ); } public void remove( OssLicenseDoc doc ) { getLicenseDocs().remove( doc ); } public void add( OssLicenseDoc doc ) { doc.setOssComponent( this ); getLicenseDocs().add( doc ); } public String getName() { return name; } public void setName( String name ) { this.name = name; } public String getVersion() { return version; } public void setVersion( String version ) { this.version = version; } public Date getCreated() { return created; } public void setCreated( Date created ) { this.created = created; } public String getDesc() { return desc; } public void setDesc( String desc ) { this.desc = desc; } public String getContentType() { return contentType; } public void setContentType( String contentType ) { this.contentType = contentType; } public String getFileName() { return fileName; } public void setFileName( String fileName ) { this.fileName = fileName; } public Long getFileSize() { return fileSize; } public void setFileSize( Long fileSize ) { this.fileSize = fileSize; } public String getFileSizeStr() { return FileSize.format( getFileSize() ); } public String toString() { return "OssComponent: ID=" + getId() + ", name=\"" + getName() + "\""; } public boolean equals( Object o ) { if (o == null) return false; if (this == o) return true; if (!(o instanceof OssComponent)) return false; OssComponent c = (OssComponent)o; return this.getId().equals( c.getId() ); } public void dump() { dump( this ); } public static void dump( OssComponent c ) { if (c == null) { System.out.println( "OssComponent: null" ); return; } System.out.println( "OssComponent:" ); System.out.println( "\tID:\t" + c.getId() ); System.out.println( "\tName:\t" + c.getName() ); System.out.println( "\tVersion:\t" + c.getVersion() ); System.out.println(); } public static void dump( List<OssComponent> lst ) { for (OssComponent oc : lst) oc.dump(); } }
package com.techlunch.newb.model; import java.io.*; import java.sql.*; import org.hibernate.*; public class OssLicenseDoc { private Long id = null; private String dlUrl = null; private String fileName = null; private String contentType = null; private Long fileSize = null; private Blob content = null; private OssComponent ossComponent = null; public OssLicenseDoc() { } private Blob getContent() { return content; } private void setContent( Blob content ) { this.content = content; } public InputStream getContentStream() throws SQLException { if (getContent() == null) return null; return getContent().getBinaryStream(); } public void setContentStream( InputStream sourceStream ) throws IOException { setContent( Hibernate.createBlob( sourceStream ) ); } public String getContentType() { return contentType; } public void setContentType( String contentType ) { this.contentType = contentType; } public String getDlUrl() { return dlUrl; } public void setDlUrl( String dlUrl ) { this.dlUrl = dlUrl; } public String getFileName() { return fileName; } public void setFileName( String fileName ) { this.fileName = fileName; } public Long getFileSize() { return fileSize; } public void setFileSize( Long fileSize ) { this.fileSize = fileSize; } public String getFileSizeStr() { return FileSize.format( getFileSize() ); } public Long getId() { return id; } private void setId( Long id ) { this.id = id; } public OssComponent getOssComponent() { return ossComponent; } public void setOssComponent( OssComponent ossComponent ) { this.ossComponent = ossComponent; } public String toString() { return "OssLicenseDoc: ID=" + getId() + ", file name=\"" + getFileName() + "\""; } public boolean equals( Object o ) { if (o == null) return false; if (this == o) return true; if (!(o instanceof OssLicenseDoc)) return false; OssLicenseDoc doc = (OssLicenseDoc)o; return this.getId().equals( doc.getId() ); } public void dump() { dump( this ); } public static void dump( OssLicenseDoc c ) { if (c == null) { System.out.println( "OssLicenseDoc: null" ); return; } System.out.println( "OssLicenseDoc:" ); System.out.println( "\tID:\t" + c.getId() ); System.out.println( "\tName:\t" + c.getFileName() ); System.out.println( "\tVersion:\t" + c.getContentType() ); System.out.println(); } }
Test what we've done
Now that we have our mappings and classes setup, we can create a simple class to show off the functionality.
package com.techlunch.newb.model.test; import com.techlunch.newb.model.*; import com.techlunch.newb.model.dao.*; import java.io.*; import java.util.*; public class FileEntityInitializer { private FileEntityInitializer() {} public static void main( String[] args ) { try { HbnConfigUtil.useLocalConfig( true, true ); HbnSessionUtil.beginTransaction(); String pathStr = "./TestFiles/stripes-1.3.2.zip"; OssComponent stripes = new OssComponent(); File distro = new File( pathStr ); FileInputStream fis = new FileInputStream( distro ); stripes.setName( "Stripes" ); stripes.setDesc( "A badass web presentation framework." ); stripes.setVersion( "1.3.1" ); stripes.setDlUrl( "http://stripes.mc4j.org/confluence/display" + "/stripes/Download" ); stripes.setCreated( new Date() ); stripes.setContentType( "application/zip" ); stripes.setFileName( distro.getName() ); stripes.setFileSize( distro.length() ); stripes.setDistroContent( fis ); OssLicenseDoc dA = new OssLicenseDoc(); OssLicenseDoc dB = new OssLicenseDoc(); File fA = new File( "./TestFiles/LICENSE-2.0.txt" ); File fB = new File( "./TestFiles/lgpl-license.html" ); FileInputStream sA = new FileInputStream( fA ); FileInputStream sB = new FileInputStream( fB ); dA.setOssComponent( stripes ); dA.setContentStream( sA ); dA.setFileName( fA.getName() ); dA.setContentType( "text/plain" ); dA.setFileSize( fA.length() ); dA.setDlUrl( "http://apache.org/licenses/LICENSE-2.0.txt" ); dB.setOssComponent( stripes ); dB.setContentStream( sB ); dB.setFileName( fB.getName() ); dB.setContentType( "text/html" ); dB.setFileSize( fB.length() ); dB.setDlUrl( "http://opensource.org/licenses/lgpl-license.php" ); stripes.add( dA ); stripes.add( dB ); DAOFactory.DEFAULT.getOssComponentDAO().makePersistent( stripes ); } catch (Exception e) { e.printStackTrace(); HbnSessionUtil.rollbackOnly(); } finally { HbnSessionUtil.resolveTransaction(); HbnSessionUtil.closeSession(); } } }
After you run this, bust open your favorite MySQL schema browser tool, and check out your file content as stored in your MySQL table rows.