Легкий старт: Spring + MongoDB

от автора

Поискал на хабре схожие статьи, нашел только Morphia — легкий ORM для MongoDB, управляемый аннотациями, ничего по связке Spring Data + MongoDB не нашлось, в связи с этим решил написать пост из раздела «для самых маленьких» по настройке и использованию связки Spring + MongoDB.

Что будет из себя представлять само приложение:

Сделаем самый простой менеджер контактов, для того, чтобы попробовать на примере элементарные операции CRUD.

Используемые библиотеки:
  • Spring IoC, MVC, Data (Mongo)
  • Mongo Driver
  • Log4j через sl4j
  • ну и немножко дополнительных, которые опишу уже в конфигурационном файле

Собирать проект будет Maven, а код, лично я, пишу в Intellij IDEA (думаю, это лучшая IDE для Java). Кстати говоря, о преимуществах этой среды в своё время рассказывал asolntsev в посте Почему IDEA лучше Eclipse.

Конфигурация проекта

Для начала опишем pom.xml:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">     <modelVersion>4.0.0</modelVersion>     <packaging>war</packaging>      <groupId>habra</groupId>     <artifactId>habr</artifactId>     <version>1.0-SNAPSHOT</version>      <!-- На момент написания статьи, версии библиотек являются последними -->     <properties>         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>         <version.jdk>1.6</version.jdk>         <version.spring>4.0.2.RELEASE</version.spring>         <version.spring.mongodb>1.4.0.RELEASE</version.spring.mongodb>         <version.jackson>1.9.13</version.jackson>     </properties>      <dependencies>         <!-- Все, что нужно для Spring -->         <dependency>             <groupId>org.springframework</groupId>             <artifactId>spring-core</artifactId>             <version>${version.spring}</version>         </dependency>          <dependency>             <groupId>org.springframework</groupId>             <artifactId>spring-web</artifactId>             <version>${version.spring}</version>         </dependency>          <dependency>             <groupId>org.springframework</groupId>             <artifactId>spring-webmvc</artifactId>             <version>${version.spring}</version>         </dependency>          <dependency>             <groupId>org.springframework</groupId>             <artifactId>spring-context</artifactId>             <version>${version.spring}</version>         </dependency>          <dependency>             <groupId>org.springframework.data</groupId>             <artifactId>spring-data-mongodb</artifactId>             <version>${version.spring.mongodb}</version>         </dependency>          <!-- MongoDB драйвер -->         <dependency>             <groupId>org.mongodb</groupId>             <artifactId>mongo-java-driver</artifactId>             <version>2.11.4</version>         </dependency>          <!--         Jackson JSON Mapper         Тащу его всегда, когда нужно писать API на базе JSON объектов.         В нашем случае можете эту библиотеку не тянуть.         -->         <dependency>             <groupId>org.codehaus.jackson</groupId>             <artifactId>jackson-mapper-asl</artifactId>             <version>${version.jackson}</version>         </dependency>          <!-- Servlet Api -->         <dependency>             <groupId>javax.servlet</groupId>             <artifactId>servlet-api</artifactId>             <version>2.5</version>             <scope>provided</scope>         </dependency>          <dependency>             <groupId>javax.servlet.jsp</groupId>             <artifactId>jsp-api</artifactId>             <version>2.1</version>             <scope>provided</scope>         </dependency>          <dependency>             <groupId>jstl</groupId>             <artifactId>jstl</artifactId>             <version>1.2</version>         </dependency>          <!-- Логгирование -->         <dependency>             <groupId>log4j</groupId>             <artifactId>log4j</artifactId>             <version>1.2.17</version>         </dependency>          <dependency>             <groupId>org.slf4j</groupId>             <artifactId>slf4j-api</artifactId>             <version>1.7.5</version>         </dependency>          <dependency>             <groupId>org.slf4j</groupId>             <artifactId>slf4j-log4j12</artifactId>             <version>1.7.5</version>         </dependency>          <!-- TEST -->         <dependency>             <groupId>junit</groupId>             <artifactId>junit</artifactId>             <version>4.11</version>             <scope>test</scope>         </dependency>          <!--         Apache Commons, тяну пратически в каждый свой проект из-за их полезности.         Можете также их пропустить.         -->         <dependency>             <groupId>commons-io</groupId>             <artifactId>commons-io</artifactId>             <version>2.4</version>         </dependency>          <dependency>             <groupId>commons-lang</groupId>             <artifactId>commons-lang</artifactId>             <version>2.6</version>         </dependency>          <dependency>             <groupId>commons-fileupload</groupId>             <artifactId>commons-fileupload</artifactId>             <version>1.3.1</version>         </dependency>     </dependencies>      <build>         <plugins>             <plugin>                 <artifactId>maven-war-plugin</artifactId>                 <version>2.4</version>                 <configuration>                     <webXml>src/main/webapp/WEB-INF/web.xml</webXml>                 </configuration>             </plugin>         </plugins>     </build> </project> 

Теперь, когда все библиотеки описаны, можно приступить к описанию конфигурационных файлов Spring.
Для этого я обычно создаю папку «spring» в src/main/resources, некоторые хранят файлы конфигурации Spring в webapp/WEB-INF/*, но это уже кому как удобно. Скажу сразу, конфигурационных файла будет 2, по крайне мере я считаю, что это наиболее верный вариант описания конфигурации. 1-й файл будет включать конфигурацию по созданию бинов, подключение к БД, и пр., так сказать конфигурация контекста всего приложения. 2-й файл — это описание работы DispatcherServlet, в общем все, что связано уже с отображением страниц, и вообще c Spring MVC.

Начнем с описания контекста всего приложения (src/main/resources/spring/applicationContext.xml):

<beans xmlns="http://www.springframework.org/schema/beans"        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xmlns:context="http://www.springframework.org/schema/context"        xmlns:mongo="http://www.springframework.org/schema/data/mongo"        xsi:schemaLocation="http://www.springframework.org/schema/beans         http://www.springframework.org/schema/beans/spring-beans.xsd         http://www.springframework.org/schema/context         http://www.springframework.org/schema/context/spring-context.xsd         http://www.springframework.org/schema/data/mongo         http://www.springframework.org/schema/data/mongo/spring-mongo.xsd">      <!-- Включаем контекстные аннотации типа @Service, @Controller, @Repository... -->     <context:annotation-config/>      <!--     Указываем Springу пакет, в котором он будет искать классы,     помеченные аннотациями @Service, @Repository, и создавать их бины, но исключать он будет @Controller,     т.к. эти классы нам нужны будут в другом месте.     -->     <context:component-scan base-package="ru.habrahabr.sm">         <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>     </context:component-scan>      <!--     Загружает properties файл в конфигурацию Spring (т.е. сюда).     Переменные из файла можно будет использовать как ${mongo.host} (пример см. ниже)     -->     <context:property-placeholder location="classpath:database.properties"/>      <!-- Создаем бин 'mongo' -->     <mongo:mongo host="${mongo.host}" port="${mongo.port}"/>      <!--     Создаем бин 'mongoDbFactory'.     Если MongoDB не требует авторизации, то поля username, password можно убрать     -->     <mongo:db-factory             username="${mongo.username}"             password="${mongo.password}"             dbname="${mongo.db}"             mongo-ref="mongo"/>      <bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">         <constructor-arg name="mongoDbFactory" ref="mongoDbFactory"/>     </bean> </beans> 

Описание контекста Spring MVC (src/main/resources/spring/dispatcherServlet.xml):

<beans xmlns="http://www.springframework.org/schema/beans"        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xmlns:context="http://www.springframework.org/schema/context"        xmlns:mvc="http://www.springframework.org/schema/mvc"        xsi:schemaLocation="http://www.springframework.org/schema/beans         http://www.springframework.org/schema/beans/spring-beans.xsd         http://www.springframework.org/schema/context         http://www.springframework.org/schema/context/spring-context.xsd         http://www.springframework.org/schema/mvc         http://www.springframework.org/schema/mvc/spring-mvc.xsd">      <!-- Включаем MVC аннотации -->     <mvc:annotation-driven/>      <!--     Использование MVC Resources     Проще говоря, все файлы из папки webapp/resources/ будут доступны по адресу: localhost/resources/     -->     <mvc:resources mapping="/resources/**" location="/resources/"/>      <!-- Указываем Spring MVC где искать классы-контроллеры -->     <context:component-scan base-package="ru.habrahabr.sm">         <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />     </context:component-scan>      <!-- Указываем Spring MVC где будут лежать наши Viewшки, в данном случае это "/WEB-INF/pages/" -->     <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">         <property name="prefix" value="/WEB-INF/pages/"/>         <property name="suffix" value=".jsp"/>     </bean> </beans> 

Отлично, Spring настроен, но если вы внимательно читали конфигурационные файлы, то заметили, что в applicationContext.xml мы тянули "classpath:database.properties". Это значит, что нужно создать файл в «src/main/resources/database.properties» со следующим содержанием:

mongo.host=localhost mongo.port=27017 mongo.db=mydb mongo.username=username mongo.password=password 

Разумеется, нужно заменить данные после знака ‘=’ своими. Заметьте, что поля username, password необязательные, и если у вас не нужна авторизация для получения доступа к MongoDB, загляните в applicationContext.xml, там указано какие поля нужно убрать. Сразу создадим конфигурационный файл для системы логгирования (src/main/resources/log4j.properties):

log4j.rootLogger=INFO, stdout  # Direct log messages to stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.conversionPattern=%d{dd.MM.yy HH:mm:ss} %5p - %m%n log4j.appender.stdout.encoding=UTF-8 

Наконец можно перейти к завершающему этапу конфигурирования Java Web приложения — описание web.xml (src/main/webapp/WEB-INF/web.xml):

<web-app version="2.4"          xmlns="http://java.sun.com/xml/ns/j2ee"          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"          xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee          http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">      <!-- Spring Application Context -->     <context-param>         <param-name>contextConfigLocation</param-name>         <param-value>classpath:spring/applicationContext.xml</param-value>     </context-param>      <listener>         <listener-class>             org.springframework.web.context.ContextLoaderListener         </listener-class>     </listener>     <!-- /Spring Application Context -->      <!-- Spring Dispatcher Servlet Context -->     <servlet>         <servlet-name>dispatcher</servlet-name>         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>         <init-param>             <param-name>contextConfigLocation</param-name>             <param-value>classpath:spring/dispatcherServlet.xml</param-value>         </init-param>         <load-on-startup>1</load-on-startup>     </servlet>      <servlet-mapping>         <servlet-name>dispatcher</servlet-name>         <url-pattern>/</url-pattern>     </servlet-mapping>     <!-- /Spring Dispatcher Servlet Context -->      <!-- Filters -->      <!-- Character Filter -->     <filter>         <filter-name>encodingFilter</filter-name>         <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>         <init-param>             <param-name>encoding</param-name>             <param-value>UTF-8</param-value>         </init-param>         <init-param>             <param-name>forceEncoding</param-name>             <param-value>true</param-value>         </init-param>     </filter>      <filter-mapping>         <filter-name>encodingFilter</filter-name>         <url-pattern>/*</url-pattern>     </filter-mapping>     <!-- /Character Filter -->      <!-- /Filters -->  </web-app> 

На данном этапе у Вас уже должна быть следующая структура файлов в проекте:
В папку «src/webapp/resources/» можете ложить любой статический контент (картинки, стили, js скрипты, и т.д.).

Написание кода

Наконец-то проект сконфигурирован, и можно приступить к написанию самого кода приложения. Начнем с описания модели и реализации слоя по работе с БД. Хочу отметить следующее: в MongoDB нет целочисленного AUTO_INCREMENT PK поля, а объекту по умолчанию присваивается ID такого вида: ObjectId(«5326b46f44ae9e6328b4566c»). Spring Data понимает этот ID как объект типа String. Проще говоря, если вам нужно сделать так, чтобы у вас ID объекта было целочисленным и авто увеличивающимся, то придется над этим поработать самостоятельно. Но на самом деле в этом нет ничего сложного и не стоит этого бояться, а я сейчас опишу как это делается! Если же Вас устраивает и длинный String в качестве ID объекта, то пропустите все классы (и соответственно их использование) в которых встречается слово «Sequence».

Для начала создаем коллекцию в БД с именем sequences. И руками вносим туда объект (insert):

{     "_id" : "contacts",     "sequence" : 0 } 

В этой коллекции мы будем хранить пару: «имя таблицы» — «последний ID», это значит, что для каждой таблицы(коллекции), в которой Вы хотите использовать нашу самописную AUTO_INCREMENT реализацию, вам нужно внести новую запись, как указано выше, только вместо «contacts» введите имя нужной Вам коллекции. Кстати, если еще не знаете какой инструмент(management-tool) использовать для MongoDB, то рекомендую Robomongo.

Теперь создаем класс-обертку для этой коллекции:

package ru.habrahabr.sm.model;  import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document;  /**  * Date: 26.03.2014  * Time: 15:38  *  * @author Ruslan Molchanov (ruslanys@gmail.com)  */ @Document(collection = Sequence.COLLECTION_NAME) public class Sequence {     public static final String COLLECTION_NAME = "sequences";      @Id     private String id;     private Long sequence;      public Sequence() {     }      public String getId() {         return id;     }      public void setId(String id) {         this.id = id;     }      public Long getSequence() {         return sequence;     }      public void setSequence(Long sequence) {         this.sequence = sequence;     } } 

Обратите внимание на запись "@Document(collection = Sequence.COLLECTION_NAME)" на самом деле можно было бы написать "@Document(collection = «sequences»)", но это уже скорее дело вкуса. Плюс есть одно явное достоинство у такого метода, дальше поймете какое.

Теперь опишем слой по работе с БД для класса-обертки "Sequence", скажу сразу, чтобы не растягивать статью, я не буду создавать интерфейсы для сервисов и DAO, буду пользоваться уже реализациями объектов (хотя это вроде не очень хорошо), надеюсь, что Вы понимаете о чем идет речь.

package ru.habrahabr.sm.dao;  import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.FindAndModifyOptions; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.stereotype.Repository; import ru.habrahabr.sm.exceptions.SequenceException; import ru.habrahabr.sm.model.Sequence;  /**  * Этот код взят с сайта mkyong,  * точную ссылку найти не смог  */ @Repository public class SequenceDao {     @Autowired private MongoOperations mongoOperations;      public Long getNextSequenceId(String key) {         // получаем объект Sequence по наименованию коллекции         Query query = new Query(Criteria.where("id").is(key));          // увеличиваем поле sequence на единицу         Update update = new Update();         update.inc("sequence", 1);          // указываем опцию, что нужно возвращать измененный объект         FindAndModifyOptions options = new FindAndModifyOptions();         options.returnNew(true);          // немного магии :)         Sequence sequence = mongoOperations.findAndModify(query, update, options, Sequence.class);          // if no sequence throws SequenceException         if(sequence == null) throw new SequenceException("Unable to get sequence for key: " + key);          return sequence.getSequence();     } } 

Теперь класс исключения, которое выбрасывается, если в нашей коллекции `sequences` не будет соответствующей записи в БД (о которой я писал выше):

package ru.habrahabr.sm.exceptions;  /**  * Date: 26.03.2014  * Time: 16:09  *  * @author Ruslan Molchanov (ruslanys@gmail.com)  */ public class SequenceException extends RuntimeException {     public SequenceException(String message) {         super(message);     } } 

Теперь создаем в БД коллекцию `contacts`, и класс-обертку:

package ru.habrahabr.sm.model;  import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document;  import java.io.Serializable;  /**  * Date: 26.03.2014  * Time: 15:29  *  * @author Ruslan Molchanov (ruslanys@gmail.com)  */ @Document(collection = Contact.COLLECTION_NAME) public class Contact implements Serializable {     public static final String COLLECTION_NAME = "contacts";      @Id     private Long id;     /* *******************************************************      Если вы хотите, чтобы ID объекта была автогенерируемая      строка (об этом я писал в посте), то опишите поле ID так:      @Id      private String id;      ********************************************************* */      private String name;     private String number;     private String email;      public Contact() {     }      public Contact(String name, String number, String email) {         this.name = name;         this.number = number;         this.email = email;     }      public Long getId() {         return id;     }      public void setId(Long id) {         this.id = id;     }      public String getName() {         return name;     }      public void setName(String name) {         this.name = name;     }      public String getNumber() {         return number;     }      public void setNumber(String number) {         this.number = number;     }      public String getEmail() {         return email;     }      public void setEmail(String email) {         this.email = email;     } } 

Теперь опишем DAO слой для нашего класса «Contact»:

package ru.habrahabr.sm.dao;  import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.stereotype.Repository; import ru.habrahabr.sm.model.Contact;  import java.util.List;  /**  * Date: 26.03.2014  * Time: 19:13  *  * @author Ruslan Molchanov (ruslanys@gmail.com)  */ @Repository public class ContactDao {     @Autowired private MongoOperations mongoOperations;      public void save(Contact contact) {         mongoOperations.save(contact);     }      public Contact get(Long id) {         return mongoOperations.findOne(Query.query(Criteria.where("id").is(id)), Contact.class);     }      public List<Contact> getAll() {         return mongoOperations.findAll(Contact.class);     }      public void remove(Long id) {         mongoOperations.remove(Query.query(Criteria.where("id").is(id)), Contact.class);     } } 

Хотелось бы отметить, что обновить запись в БД можно на уровне DAO следующим образом:

mongoOperations.updateFirst(query, update, Contact.class); 

Разобраться с этим самостоятельно проще простого и не составит для Вас трудностей. Но хотелось бы отметить, что если вы выполните:

mongoOperations.save(contact); 

на объект, у которого ID уже выставлен, и существует в БД, то произойдет перезапись объекта в БД под указанным ID.

Теперь опишем слой бизнес-логики для класса «Contact»:

package ru.habrahabr.sm.services;  import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import ru.habrahabr.sm.dao.ContactDao; import ru.habrahabr.sm.dao.SequenceDao; import ru.habrahabr.sm.model.Contact;  import java.util.List;  /**  * Date: 26.03.2014  * Time: 20:09  *  * @author Ruslan Molchanov (ruslanys@gmail.com)  */ @Service public class ContactService {     @Autowired private SequenceDao sequenceDao;     @Autowired private ContactDao contactDao;          public void add(Contact contact) {         contact.setId(sequenceDao.getNextSequenceId(Contact.COLLECTION_NAME));         contactDao.save(contact);     }          public void update(Contact contact) {         contactDao.save(contact);     }          public Contact get(Long id) {         return contactDao.get(id);     }          public List<Contact> getAll() {         return contactDao.getAll();     }          public void remove(Long id) {         contactDao.remove(id);     } } 

Ну вот, собственно и все, все, что касается связки Spring + MongoDB мы проделали, т.е. теперь Вы можете пользоваться ORM Spring Data с уже подключенной к проекту MongoDB, но раз я обещал на примере справочника показать работу с БД, то осталось написать еще класс-контроллер:

package ru.habrahabr.sm.web;  import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; import ru.habrahabr.sm.model.Contact; import ru.habrahabr.sm.services.ContactService;  /**  * Date: 26.03.2014  * Time: 20:30  *  * @author Ruslan Molchanov (ruslanys@gmail.com)  */ @Controller public class MainController {     @Autowired private ContactService contactService;      @RequestMapping(value = "/", method = RequestMethod.GET)     public ModelAndView showAll() {         ModelAndView modelAndView = new ModelAndView("all");          modelAndView.addObject("contacts", contactService.getAll());          return modelAndView;     }      @RequestMapping(value = "/add", method = RequestMethod.GET)     public ModelAndView showAddForm() {         return new ModelAndView("add_form", "contact", new Contact());     }      @RequestMapping(value = "/add", method = RequestMethod.POST)     public String addContact(@ModelAttribute("contact") Contact contact) {         if(contact.getId() == null) contactService.add(contact);         else contactService.update(contact);          return "redirect:/";     }      @RequestMapping(value = "/edit", method = RequestMethod.GET)     public ModelAndView showEditForm(@RequestParam(required = true) Long id) {         return new ModelAndView("add_form", "contact", contactService.get(id));     }      @RequestMapping(value = "/delete", method = RequestMethod.GET)     public String deleteContact(@RequestParam(required = true) Long id) {         contactService.remove(id);          return "redirect:/";     } } 

Ну и вьюшки (src/webapp/WEB-INF/pages/add_form.jsp):

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head>     <title>Добавить контакт</title> </head> <body> <form:form method="POST" action="/add" modelAttribute="contact">     <form:hidden path="id" />     <table>         <tr>             <td>Name:</td>             <td><form:input path="name" /></td>         </tr>         <tr>             <td>Number:</td>             <td><form:input path="number" /></td>         </tr>         <tr>             <td>E-mail:</td>             <td><form:input path="email" /></td>         </tr>         <tr>             <td colspan="2">                 <input type="submit" />             </td>         </tr>     </table> </form:form> </body> </html> 

Еще одна (src/webapp/WEB-INF/all.jsp):

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>  <html> <head>     <title>Все контакты</title> </head> <body> <table width="600px">     <tr>         <td><b>ID</b></td>         <td><b>Name</b></td>         <td><b>Number</b></td>         <td><b>E-mail</b></td>         <td><b>Action</b></td>     </tr>     <c:forEach var="contact" items="${contacts}">         <tr>             <td>${contact.id}</td>             <td>${contact.name}</td>             <td>${contact.number}</td>             <td>${contact.email}</td>             <td><a href="/edit?id=${contact.id}">Edit</a> | <a href="/delete?id=${contact.id}">Delete</a></td>         </tr>     </c:forEach>     <tr>         <td colspan="5">             <a href="/add">Добавить запись</a>         </td>     </tr> </table> </body> </html> 

Ну вот и все, собираем проект, запускаем, и вуаля:

Ссылка на исходники: db.tt/2zfWWsJT

P.S. Буду счастлив, если кому-нибудь пригодится этот пост. Буду благодарен за подсказки и исправления.

ссылка на оригинал статьи http://habrahabr.ru/post/217391/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *