├── .gitignore ├── Procfile ├── README.md ├── pom.xml ├── src └── main │ ├── java │ ├── com │ │ └── jamesward │ │ │ ├── Webapp.java │ │ │ ├── config │ │ │ ├── DataConfig.java │ │ │ ├── RootConfig.java │ │ │ └── WebConfig.java │ │ │ ├── controller │ │ │ └── ContactController.java │ │ │ ├── model │ │ │ └── Contact.java │ │ │ └── service │ │ │ ├── ContactService.java │ │ │ └── ContactServiceImpl.java │ └── net │ │ └── backtothefront │ │ ├── HstoreHelper.java │ │ └── HstoreUserType.java │ └── resources │ └── META-INF │ └── resources │ └── index.html └── system.properties /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.classpath 3 | /.project 4 | /.settings 5 | /tomcat.* 6 | /.idea 7 | /*.iml 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java $JAVA_OPTS -cp target/classes:target/dependency/* com.jamesward.Webapp -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NoSQL Inside SQL with Java, Spring, Hibernate, and PostgreSQL 2 | ============================================================= 3 | 4 | There are many benefits to schema-less NoSQL datastores, but there are always trade-offs. The primary gift the NoSQL movement has given us is the variety of options we now have for data persistence, instead of always trying to shoehorn everything into a relational model. Now the challenge is in deciding which persistence model fits best with each domain in a system and then combining those models in a cohesive way. The general term to describe this is [Polyglot Persistence](http://martinfowler.com/bliki/PolyglotPersistence.html) and there are many ways to accomplish it. Lets walk through how you can combine a regular SQL model with a key-value NoSQL model using Java, Spring, Hibernate, and PostgreSQL. 5 | 6 | This article will walk through the pieces of a simple web application that uses regular SQL and [PostgreSQL's hstore](http://www.postgresql.org/docs/9.1/static/hstore.html) for key value pairs. This method is a mix of NoSQL inside SQL. One benefit of this approach is that the same datastore can be used for both the SQL and the NoSQL data. 7 | 8 | In this example the server technologies will be Java, Spring, and Hibernate. (The same thing can also be done with [Rails](http://schneems.com/post/19298469372/you-got-nosql-in-my-postgres-using-hstore-in-rails), [Django](http://craigkerstiens.com/2012/06/11/schemaless-django/), and many other technologies.) To add Hibernate support for `hstore` I found a fantastic blog about "[Storing sets of key/value pairs in a single db column with Hibernate using PostgreSQL hstore type](http://backtothefront.net/2011/storing-sets-keyvalue-pairs-single-db-column-hibernate-postgresql-hstore-type/)". I won't go through that code here but you can find everything in the [GitHub repo for my demo project](https://github.com/jamesward/spring_hibernate_hstore_demo). 9 | 10 | This demo app uses Maven to define [the dependencies](https://github.com/jamesward/spring_hibernate_hstore_demo/blob/master/pom.xml). Embedded Jetty is started via a [plain 'ole Java application](https://github.com/jamesward/spring_hibernate_hstore_demo/blob/master/src/main/java/com/jamesward/Webapp.java) that sets up Spring MVC. Spring is configured via Java Config for the [main stuff](https://github.com/jamesward/spring_hibernate_hstore_demo/blob/master/src/main/java/com/jamesward/config/RootConfig.java), the [web stuff](https://github.com/jamesward/spring_hibernate_hstore_demo/blob/master/src/main/java/com/jamesward/config/WebConfig.java), and the [database stuff](https://github.com/jamesward/spring_hibernate_hstore_demo/blob/master/src/main/java/com/jamesward/config/DataConfig.java). 11 | 12 | The client technologies will be jQuery and Bootstrap and there is a strict seperation between the client and server via RESTful JSON services. The whole client-side is in a [plain 'ole HTML file](https://github.com/jamesward/spring_hibernate_hstore_demo/blob/master/src/main/resources/META-INF/resources/index.html). Via jQuery / Ajax the client communicates to JSON services exposed via a [Spring MVC Controller](https://github.com/jamesward/spring_hibernate_hstore_demo/blob/master/src/main/java/com/jamesward/controller/ContactController.java). 13 | 14 | Ok. Now onto the NoSQL inside SQL stuff. This application stores "Contacts" that have a name but also can have many "Contact Methods" (e.g. phone numbers and email addresses). The "Contact Methods" are a good use of a schema-less, key-value pair column because it avoids the cumbersome alternatives: putting that information into a separate table or trying to create a model object that has all of the possible "Contact Methods". So lets take a look at the simple [Contact Entity](https://github.com/jamesward/spring_hibernate_hstore_demo/blob/master/src/main/java/com/jamesward/model/Contact.java): 15 | 16 | ``` 17 | package com.jamesward.model; 18 | 19 | import net.backtothefront.HstoreUserType; 20 | import org.hibernate.annotations.Type; 21 | import org.hibernate.annotations.TypeDef; 22 | 23 | import javax.persistence.Column; 24 | import javax.persistence.Entity; 25 | import javax.persistence.GeneratedValue; 26 | import javax.persistence.Id; 27 | import java.util.HashMap; 28 | import java.util.Map; 29 | 30 | @Entity 31 | @TypeDef(name = "hstore", typeClass = HstoreUserType.class) 32 | public class Contact { 33 | 34 | @Id 35 | @GeneratedValue 36 | public Integer id; 37 | 38 | @Column(nullable = false) 39 | public String name; 40 | 41 | @Type(type = "hstore") 42 | @Column(columnDefinition = "hstore") 43 | public Map contactMethods = new HashMap(); 44 | 45 | } 46 | ``` 47 | 48 | If you are familiar with Hibernate / JPA then most of this should look pretty familiar to you. The new / interesting stuff is the `contactMethods` property. It is a `Map` and it uses PostgreSQL's `hstore` datatype. In order for that to work, the type has to be defined and the `columnDefinition` set. Thanks again to [Jakub Głuszecki](http://backtothefront.net/) for putting together the [HstoreHelper](https://github.com/jamesward/spring_hibernate_hstore_demo/blob/master/src/main/java/net/backtothefront/HstoreHelper.java) and [HstoreUserType](https://github.com/jamesward/spring_hibernate_hstore_demo/blob/master/src/main/java/net/backtothefront/HstoreUserType.java) that make this possible. 49 | 50 | Now the rest is simple because it's just plain Hibernate / JPA. Here is the [ContactService](https://github.com/jamesward/spring_hibernate_hstore_demo/blob/master/src/main/java/com/jamesward/service/ContactServiceImpl.java) that does the basic query and updates: 51 | 52 | ``` 53 | package com.jamesward.service; 54 | 55 | import com.jamesward.model.Contact; 56 | import org.springframework.stereotype.Service; 57 | import org.springframework.transaction.annotation.Transactional; 58 | 59 | import javax.persistence.EntityManager; 60 | import javax.persistence.PersistenceContext; 61 | import javax.persistence.criteria.CriteriaQuery; 62 | 63 | import java.util.List; 64 | 65 | @Service 66 | @Transactional 67 | public class ContactServiceImpl implements ContactService { 68 | 69 | @PersistenceContext 70 | EntityManager em; 71 | 72 | @Override 73 | public void addContact(Contact contact) { 74 | em.persist(contact); 75 | } 76 | 77 | @Override 78 | public List getAllContacts() { 79 | CriteriaQuery c = em.getCriteriaBuilder().createQuery(Contact.class); 80 | c.from(Contact.class); 81 | return em.createQuery(c).getResultList(); 82 | } 83 | 84 | public Contact getContact(Integer id) { 85 | return em.find(Contact.class, id); 86 | } 87 | 88 | @Override 89 | public void addContactMethod(Integer contactId, String name, String value) { 90 | Contact contact = getContact(contactId); 91 | contact.contactMethods.put(name, value); 92 | } 93 | 94 | } 95 | ``` 96 | 97 | Now that you understand how it all works, check out a [live demo on Heroku](http://immense-crag-5799.herokuapp.com/). 98 | 99 | If you want to run this app locally or on Heroku, then first you need to grab the source code and continue working inside the newly created `spring_hibernate_hstore_demo` directory: 100 | 101 | $ git clone https://github.com/jamesward/spring_hibernate_hstore_demo.git 102 | $ cd spring_hibernate_hstore_demo 103 | 104 | To run locally: 105 | 106 | 1. Setup your PostgreSQL database to support hstore by opening a `psql` connection to it: 107 | 108 | $ psql -U username -W -h localhost database 109 | 110 | 2. Then enable hstore: 111 | 112 | => create extension hstore; 113 | => \q 114 | 115 | 3. Build the app (depends on having [Maven installed](http://maven.apache.org)): 116 | 117 | $ mvn package 118 | 119 | 4. Set the `DATABASE_URL` environment variable to point to your PostgreSQL server: 120 | 121 | $ export DATABASE_URL=postgres://username:password@localhost/databasename 122 | 123 | 5. Start the app: 124 | 125 | $ java -cp target/classes:target/dependency/* com.jamesward.Webapp 126 | 127 | 6. [Try it out](http://localhost:8080) 128 | 129 | Cool! Now you can run it on the cloud with Heroku. Here is what you need to do: 130 | 131 | 1. [Install the Heroku Toolbelt](http://toolbelt.heroku.com) 132 | 133 | 2. Login to Heroku: 134 | 135 | $ heroku login 136 | 137 | 3. Create a new app: 138 | 139 | $ heroku create 140 | 141 | 4. Add Heroku Postgres: 142 | 143 | $ heroku addons:add heroku-postgresql:dev 144 | 145 | 5. Tell Heroku to set the `DATABASE_URL` environment variable based on the database that was just added (replace `YOUR_HEROKU_POSTGRESQL_COLOR_URL` with your own): 146 | 147 | $ heroku pg:promote YOUR_HEROKU_POSTGRESQL_COLOR_URL 148 | 149 | 6. Open a `psql` connection to the database: 150 | 151 | $ heroku pg:psql 152 | 153 | 7. Enable `hstore` support in your database: 154 | 155 | => create extension hstore; 156 | => \q 157 | 158 | 8. Deploy the app: 159 | 160 | $ git push heroku master 161 | 162 | 9. View the app on the cloud: 163 | 164 | $ heroku open 165 | 166 | Fantastic! Let me know if you have any questions. 167 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.jamesward 5 | spring_hibernate_hstore_demo 6 | 0.0.1-SNAPSHOT 7 | 8 | 9 | 3.1.2.RELEASE 10 | UTF-8 11 | 12 | 13 | 14 | 15 | webjars 16 | http://webjars.github.com/m2 17 | 18 | 19 | 20 | 21 | 22 | org.springframework 23 | spring-webmvc 24 | ${org.springframework.version} 25 | 26 | 27 | org.springframework 28 | spring-orm 29 | ${org.springframework.version} 30 | 31 | 32 | 33 | org.codehaus.jackson 34 | jackson-mapper-asl 35 | 1.9.10 36 | 37 | 38 | 39 | org.hibernate 40 | hibernate-entitymanager 41 | 4.1.7.Final 42 | 43 | 44 | cglib 45 | cglib 46 | 2.2.2 47 | runtime 48 | 49 | 50 | 51 | postgresql 52 | postgresql 53 | 9.1-901-1.jdbc4 54 | 55 | 56 | 57 | org.eclipse.jetty 58 | jetty-webapp 59 | 8.1.3.v20120416 60 | 61 | 62 | 63 | taglibs 64 | standard 65 | 1.1.2 66 | 67 | 68 | javax.servlet 69 | jstl 70 | 1.2 71 | 72 | 73 | 74 | org.webjars 75 | bootstrap 76 | 2.1.1 77 | 78 | 79 | 80 | 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-dependency-plugin 85 | 2.4 86 | 87 | 88 | package 89 | 90 | copy-dependencies 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/main/java/com/jamesward/Webapp.java: -------------------------------------------------------------------------------- 1 | package com.jamesward; 2 | 3 | import com.jamesward.config.DataConfig; 4 | import com.jamesward.config.RootConfig; 5 | import com.jamesward.config.WebConfig; 6 | import org.eclipse.jetty.server.Server; 7 | import org.eclipse.jetty.servlet.ServletContextHandler; 8 | import org.eclipse.jetty.servlet.ServletHolder; 9 | import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; 10 | import org.springframework.web.servlet.DispatcherServlet; 11 | 12 | public class Webapp { 13 | 14 | public static void main(String[] args) throws Exception { 15 | 16 | final AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext(); 17 | applicationContext.register(RootConfig.class, WebConfig.class, DataConfig.class); 18 | 19 | final ServletHolder servletHolder = new ServletHolder(new DispatcherServlet(applicationContext)); 20 | final ServletContextHandler context = new ServletContextHandler(); 21 | context.setContextPath("/"); 22 | context.addServlet(servletHolder, "/*"); 23 | 24 | String webPort = System.getenv("PORT"); 25 | if (webPort == null || webPort.isEmpty()) { 26 | webPort = "8080"; 27 | } 28 | 29 | final Server server = new Server(Integer.valueOf(webPort)); 30 | 31 | server.setHandler(context); 32 | 33 | server.start(); 34 | server.join(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/jamesward/config/DataConfig.java: -------------------------------------------------------------------------------- 1 | package com.jamesward.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.jdbc.datasource.DriverManagerDataSource; 6 | import org.springframework.orm.jpa.JpaTransactionManager; 7 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 8 | import org.springframework.orm.jpa.vendor.Database; 9 | import org.springframework.orm.jpa.vendor.HibernateJpaDialect; 10 | import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; 11 | import org.springframework.transaction.PlatformTransactionManager; 12 | import org.springframework.transaction.annotation.EnableTransactionManagement; 13 | 14 | import javax.persistence.EntityManagerFactory; 15 | import javax.sql.DataSource; 16 | import java.net.URI; 17 | import java.net.URISyntaxException; 18 | import java.util.Properties; 19 | 20 | @Configuration 21 | @EnableTransactionManagement 22 | public class DataConfig { 23 | 24 | @Bean 25 | public EntityManagerFactory entityManagerFactory() throws URISyntaxException { 26 | LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean(); 27 | entityManagerFactory.setDataSource(dataSource()); 28 | entityManagerFactory.setPackagesToScan("com.jamesward.model"); 29 | HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); 30 | vendorAdapter.setShowSql(true); 31 | vendorAdapter.setDatabase(Database.POSTGRESQL); 32 | vendorAdapter.setGenerateDdl(true); 33 | entityManagerFactory.setJpaVendorAdapter(vendorAdapter); 34 | entityManagerFactory.afterPropertiesSet(); 35 | return entityManagerFactory.getObject(); 36 | } 37 | 38 | @Bean 39 | public PlatformTransactionManager transactionManager() throws URISyntaxException { 40 | JpaTransactionManager transactionManager = new JpaTransactionManager(entityManagerFactory()); 41 | transactionManager.setDataSource(dataSource()); 42 | transactionManager.setJpaDialect(new HibernateJpaDialect()); 43 | return transactionManager; 44 | } 45 | 46 | @Bean 47 | public Properties hibernateProperties() { 48 | Properties properties = new Properties(); 49 | properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect"); 50 | properties.put("hibernate.show_sql", "true"); 51 | properties.put("hibernate.hbm2ddl.auto", "update"); 52 | return properties; 53 | } 54 | 55 | @Bean 56 | public DataSource dataSource() throws URISyntaxException { 57 | final URI dbUrl = new URI(System.getenv("DATABASE_URL")); 58 | final DriverManagerDataSource dataSource = new DriverManagerDataSource(); 59 | dataSource.setDriverClassName("org.postgresql.Driver"); 60 | dataSource.setUrl("jdbc:postgresql://" + dbUrl.getHost() + dbUrl.getPath()); 61 | dataSource.setUsername(dbUrl.getUserInfo().split(":")[0]); 62 | dataSource.setPassword(dbUrl.getUserInfo().split(":")[1]); 63 | return dataSource; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/jamesward/config/RootConfig.java: -------------------------------------------------------------------------------- 1 | package com.jamesward.config; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @ComponentScan(basePackages="com.jamesward") 8 | public class RootConfig { 9 | 10 | } -------------------------------------------------------------------------------- /src/main/java/com/jamesward/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.jamesward.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 5 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 6 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 8 | 9 | @Configuration 10 | @EnableWebMvc 11 | public class WebConfig extends WebMvcConfigurerAdapter { 12 | 13 | @Override 14 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 15 | registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/"); 16 | } 17 | 18 | @Override 19 | public void addViewControllers(ViewControllerRegistry registry) { 20 | registry.addViewController("/").setViewName("redirect:index.html"); 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/main/java/com/jamesward/controller/ContactController.java: -------------------------------------------------------------------------------- 1 | package com.jamesward.controller; 2 | 3 | import com.jamesward.model.Contact; 4 | import com.jamesward.service.ContactService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | @Controller 15 | @RequestMapping(value="/api/contact") 16 | public class ContactController { 17 | 18 | @Autowired 19 | private ContactService contactService; 20 | 21 | @RequestMapping(method=RequestMethod.GET, produces=MediaType.APPLICATION_JSON_VALUE) 22 | @ResponseBody 23 | public List getAllContacts() { 24 | return contactService.getAllContacts(); 25 | } 26 | 27 | @RequestMapping(method=RequestMethod.POST) 28 | @ResponseStatus(HttpStatus.CREATED) 29 | public final void create(@RequestBody final Contact contact) { 30 | contactService.addContact(contact); 31 | } 32 | 33 | @RequestMapping(value="/{id}/contact-method", method=RequestMethod.POST) 34 | @ResponseStatus(HttpStatus.CREATED) 35 | public final void addContactMethod(@PathVariable("id") final Integer contactId, @RequestBody final Map data) { 36 | contactService.addContactMethod(contactId, data.get("name"), data.get("value")); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/jamesward/model/Contact.java: -------------------------------------------------------------------------------- 1 | package com.jamesward.model; 2 | 3 | import net.backtothefront.HstoreUserType; 4 | import org.hibernate.annotations.Type; 5 | import org.hibernate.annotations.TypeDef; 6 | 7 | import javax.persistence.Column; 8 | import javax.persistence.Entity; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.Id; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | @Entity 15 | @TypeDef(name = "hstore", typeClass = HstoreUserType.class) 16 | public class Contact { 17 | 18 | @Id 19 | @GeneratedValue 20 | public Integer id; 21 | 22 | @Column(nullable = false) 23 | public String name; 24 | 25 | @Type(type = "hstore") 26 | @Column(columnDefinition = "hstore") 27 | public Map contactMethods = new HashMap(); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/jamesward/service/ContactService.java: -------------------------------------------------------------------------------- 1 | package com.jamesward.service; 2 | 3 | 4 | import java.util.List; 5 | 6 | import com.jamesward.model.Contact; 7 | 8 | public interface ContactService { 9 | 10 | public void addContact(Contact contact); 11 | public List getAllContacts(); 12 | public void addContactMethod(Integer contactId, String name, String value); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/jamesward/service/ContactServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.jamesward.service; 2 | 3 | import com.jamesward.model.Contact; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.Transactional; 6 | 7 | import javax.persistence.EntityManager; 8 | import javax.persistence.PersistenceContext; 9 | import javax.persistence.criteria.CriteriaQuery; 10 | 11 | import java.util.List; 12 | 13 | @Service 14 | @Transactional 15 | public class ContactServiceImpl implements ContactService { 16 | 17 | @PersistenceContext 18 | EntityManager em; 19 | 20 | @Override 21 | public void addContact(Contact contact) { 22 | em.persist(contact); 23 | } 24 | 25 | @Override 26 | public List getAllContacts() { 27 | CriteriaQuery c = em.getCriteriaBuilder().createQuery(Contact.class); 28 | c.from(Contact.class); 29 | return em.createQuery(c).getResultList(); 30 | } 31 | 32 | public Contact getContact(Integer id) { 33 | return em.find(Contact.class, id); 34 | } 35 | 36 | @Override 37 | public void addContactMethod(Integer contactId, String name, String value) { 38 | Contact contact = getContact(contactId); 39 | contact.contactMethods.put(name, value); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/net/backtothefront/HstoreHelper.java: -------------------------------------------------------------------------------- 1 | package net.backtothefront; 2 | 3 | 4 | import org.springframework.util.StringUtils; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | // courtesy of: http://backtothefront.net/2011/storing-sets-keyvalue-pairs-single-db-column-hibernate-postgresql-hstore-type/ 10 | public class HstoreHelper { 11 | 12 | private static final String K_V_SEPARATOR = "=>"; 13 | 14 | public static String toString(Map m) { 15 | if (m.isEmpty()) { 16 | return ""; 17 | } 18 | StringBuilder sb = new StringBuilder(); 19 | int n = m.size(); 20 | for (String key : m.keySet()) { 21 | sb.append("\"" + key + "\"" + K_V_SEPARATOR + "\"" + m.get(key) + "\""); 22 | if (n > 1) { 23 | sb.append(", "); 24 | n--; 25 | } 26 | } 27 | return sb.toString(); 28 | } 29 | 30 | public static Map toMap(String s) { 31 | Map m = new HashMap(); 32 | if (! StringUtils.hasText(s)) { 33 | return m; 34 | } 35 | String[] tokens = s.split(", "); 36 | for (String token : tokens) { 37 | String[] kv = token.split(K_V_SEPARATOR); 38 | String k = kv[0]; 39 | k = k.trim().substring(1, k.length() - 1); 40 | String v = kv[1]; 41 | v = v.trim().substring(1, v.length() - 1); 42 | m.put(k, v); 43 | } 44 | return m; 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/net/backtothefront/HstoreUserType.java: -------------------------------------------------------------------------------- 1 | package net.backtothefront; 2 | 3 | import org.hibernate.HibernateException; 4 | import org.hibernate.engine.spi.SessionImplementor; 5 | import org.hibernate.usertype.UserType; 6 | 7 | import java.io.Serializable; 8 | import java.sql.PreparedStatement; 9 | import java.sql.ResultSet; 10 | import java.sql.SQLException; 11 | import java.sql.Types; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | // courtesy of: http://backtothefront.net/2011/storing-sets-keyvalue-pairs-single-db-column-hibernate-postgresql-hstore-type/ 16 | public class HstoreUserType implements UserType { 17 | 18 | public Object assemble(Serializable cached, Object owner) 19 | throws HibernateException { 20 | return cached; 21 | } 22 | 23 | public Object deepCopy(Object o) throws HibernateException { 24 | // It's not a true deep copy, but we store only String instances, and they 25 | // are immutable, so it should be OK 26 | Map m = (Map) o; 27 | return new HashMap(m); 28 | } 29 | 30 | public Serializable disassemble(Object o) throws HibernateException { 31 | return (Serializable) o; 32 | } 33 | 34 | public boolean equals(Object o1, Object o2) throws HibernateException { 35 | Map m1 = (Map) o1; 36 | Map m2 = (Map) o2; 37 | return m1.equals(m2); 38 | } 39 | 40 | public int hashCode(Object o) throws HibernateException { 41 | return o.hashCode(); 42 | } 43 | 44 | @Override 45 | public Object nullSafeGet(ResultSet resultSet, String[] strings, SessionImplementor sessionImplementor, Object o) throws HibernateException, SQLException { 46 | String col = strings[0]; 47 | String val = resultSet.getString(col); 48 | return HstoreHelper.toMap(val); 49 | } 50 | 51 | @Override 52 | public void nullSafeSet(PreparedStatement preparedStatement, Object o, int i, SessionImplementor sessionImplementor) throws HibernateException, SQLException { 53 | String s = HstoreHelper.toString((Map) o); 54 | preparedStatement.setObject(i, s, Types.OTHER); 55 | } 56 | 57 | public boolean isMutable() { 58 | return true; 59 | } 60 | 61 | public Object replace(Object original, Object target, Object owner) 62 | throws HibernateException { 63 | return original; 64 | } 65 | 66 | public Class returnedClass() { 67 | return Map.class; 68 | } 69 | 70 | public int[] sqlTypes() { 71 | /* 72 | * i'm not sure what value should be used here, but it works, AFAIK only 73 | * length of this array matters, as it is a column span (1 in our case) 74 | */ 75 | return new int[] { Types.INTEGER }; 76 | } 77 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Contacts 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 84 | 85 | 86 | 87 | 94 | 95 |
96 | 97 |
98 | 99 |
100 | 101 |
102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=1.6 --------------------------------------------------------------------------------