Восьмая статья серии посвященной технологии JavaEE. В этой статье мы продолжем изучать возможности EJB, реализовав функционал страницы регистрации пользователей. Для этого нам понадобится формирование собственного запроса к БД с возможностью добавления параметром, добавление новых записей в базу, а так же использование транзакций для обеспечения отказоустойчивости операций. Всю отвественность за Сохраняемость компонентов мы возможем на контейнер в котором лежит данный компонент, то есть будем использовать "Container Managed Persistence".
Бизнесс логика нашего приложения расширяется и пришло время создать собственный сессионый компонент который будет отвечать за функционал регистрации. Напомню что до этого мы создавали уже сессионые компоненты-фасады сущностных классов с использованием автоматизированных средств IDE.
Прежде чем приступить к работе, необходимо произвести кое-какие манипуляции с нашей базой данных, а именно добавить еще одну таблицу contacts.
Зделать этом можно выполнив следующею команду в mysql shell'e:
CREATE TABLE `myblog`.`Contacts` (
`id` INT NOT NULL AUTO_INCREMENT ,
`login` VARCHAR(45) NOT NULL ,
`name` VARCHAR(45) NOT NULL ,
`value` VARCHAR(60) NOT NULL ,
PRIMARY KEY (`id`) ,
INDEX `FK_Login_Contacts` (`login` ASC) ,
UNIQUE INDEX `value_UNIQUE` (`value` ASC) ,
CONSTRAINT `FK_Login_Contacts`
FOREIGN KEY (`login` )
REFERENCES `myblog`.`users` (`login` )
ON DELETE CASCADE
ON UPDATE CASCADE)
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_general_ci
COMMENT = 'Таблица с контактными данными пользователей';
или импортировав файл командой:
mysql -uroot -p <contacts.sql
Назначение таблицы понятно из комментариев в скрипте.
Создайте для новой таблицы Entity класс и Session Facade аналогично как это делали в 6 шестой статье с использованием встроенного мастера.
Листинг класса Contacts:
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package entity;
import java.io.Serializable;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author Egorov A.
*/
@Entity
@Table(name = "contacts")
@XmlRootElement
@NamedQueries({
@NamedQuery(name = "Contacts.findAll", query = "SELECT c FROM Contacts c"),
@NamedQuery(name = "Contacts.findById", query = "SELECT c FROM Contacts c WHERE c.id = :id"),
@NamedQuery(name = "Contacts.findByName", query = "SELECT c FROM Contacts c WHERE c.name = :name"),
@NamedQuery(name = "Contacts.findByValue", query = "SELECT c FROM Contacts c WHERE c.value = :value")})
public class Contacts implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "id")
private Integer id;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 45)
@Column(name = "name")
private String name;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 60)
@Column(name = "value")
private String value;
@JoinColumn(name = "login", referencedColumnName = "login")
@ManyToOne(optional = false)
private Users login;
public Contacts() {
}
public Contacts(Integer id) {
this.id = id;
}
public Contacts(Integer id, String name, String value) {
this.id = id;
this.name = name;
this.value = value;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Users getLogin() {
return login;
}
public void setLogin(Users login) {
this.login = login;
}
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof Contacts)) {
return false;
}
Contacts other = (Contacts) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
@Override
public String toString() {
return "entity.Contacts[ id=" + id + " ]";
}
}
Листинг класса ContactsFacade:
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package session;
import entity.Contacts;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
/**
*
* @author Egorov A.
*/
@Stateless
public class ContactsFacade extends AbstractFacade<Contacts> {
@PersistenceContext(unitName = "myblogPU")
private EntityManager em;
@Override
protected EntityManager getEntityManager() {
return em;
}
public ContactsFacade() {
super(Contacts.class);
}
}
Подготовка страницы регистрации
Далее откроем файл registration.jsp и посмотрим чего в нем не хватает, а не хватает в нем поля вывода сообщений пользователю, например при уже имеющемся в базе логине или не совпадение паролей.
Добавьте следующий код перед открывающимся тегом формы:
<div class="text-article">
<c:if test="${notif ne null}">
<div class="notif">
<span>${notif}</span>
</div>
</c:if>
<form method="POST" action="registration">
Теперь если мы из сервлета передадим в запрос атрибут notif то он отобразится как сообщение пользователю.
В таблюцу стилей style.css добавьте правила:
.notif{
width: 94%;
margin-left: 2%;
border: 1px solid #660000;
background: #ff9999;
border-radius: 5px;
}
.notif span{
font-size: 16px;
font-weight: bold;
letter-spacing: 1px;
margin: 5px;
color: #660033;
}
В настоящих системах так же были бы добавлены javascript'ы для проверки корректности вводимых данных еще на строне клиента, в том числе резидентное обращение к серверу с помощью ajax, но наша цель это познанение Java EE и потому распыляться на сторонние технологии считаю излишним. Проверка корректности данных будет проиходить исключительно на сервере и в случае ошибки будет выводиться то самое сообщение пользователю которое было сделанно выше.
Добавление нового пользователя. Создание сессионного компонента
Как было оговоренно ранее, вся бизнес-логика находится в в сессионых компонентах, поэтому нашей задачей сейчас является:
- Собрать пользовательские данные в сервлете.
- Проверить их на достаточность и вызвать соотвествующий метод из EJB компонента.
- Создать сессионый компонент с реализацией функции добавления нового пользователя.
- При добавлении пользователя нужно выполнять все операции в контексте одной транзакции.
Необходимость транзакции обусловленно тем что нам придется добавлять данные сразу в две таблицы users и contacts.
Создадим новый сессионый компонент UsersManager.
- Создайте новый файл , в списке категорий выберите "Enterprise JavaBean", а в списке типов файлов "Сеансовый компонент":
- В следующем шаге введите имя EJB компонента UserManager и выбирите тип сеанса без сохранения состояния:
- IDE создало каркас EJB компонента с одной аннтоацией говорящей что это компонент имеет тип сеанса без сохранения состояния:
package session;
import javax.ejb.Stateless;
/**
*
* @author Egorov A.
*/
@Stateless
public class UsersManager {
}
- Сразу после имеющийся аннотации добавьте следующию:
@TransactionManagement(TransactionManagementType.CONTAINER)
Этой аннотацией мы говорим что все вопросами транзакцией будет заниматься контейнер.
- Внутри класса UsersManages нужно создать EntityManager, ровно таким же образом как и в другим EJB компонентах:
@PersistenceContext(unitName = "myblogPU")
private EntityManager em;
- Далее созданим еще не знакомый нам ресурс SessionContext:
@Resource
private SessionContext context;
SessionContext - класс реализующий интерфейс EJBContext. Он предоставляет информацию о контейнере, клиенте, использующем компонент и о самом компоненте. Для Entity классов данный интерфейс реализован подклассом EntityContext.
Добавление бизнес-метода
- Пришло время добавить новый бизнесс метод, вставьте в тело класса следующий код:
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Integer addUser(final String login, final String password, final String passwordTwo, final Map<String, String[]> contacts) {
try{
if (password.equals(passwordTwo)){
Users user=newUser(login, password);
newContacts(user,contacts);
return 0;
}else {return 2;}
}catch (Exception e){
context.setRollbackOnly();
e.printStackTrace();
return 1;
}
}
Объявленный бизнес-метод принемает в качестве параметров логин, пароль и список контактов (мы подразумеваем что можно будет добавлять сколько угодное количество различных контактов за раз).
Перед методом идет аннотация объявляющая что все действия данной функции будут проходить в рамках одной транзакции. Рассмотрим подробнее возможные аттрибуты транзакции:
Атрибуты транзакции
Атрибут |
Описание |
Required |
Методы выполняются внутри транзакции. Если клиент ассоциирован с транзакцией, то используется эта транзакция. Если нет, генерируется новая транзакция. Происхоидит коммит в конце метода, который означает, что метод, имеющий включенный атрибут required, но был вызван когда транзакция уже была начата, не будет вкомичен по завершении метода. |
RequiresNew |
Методы выполняются внутри транзакции. Если клиент предоставляет транзакцию, она приостанавливается. Если нет – генерируется новая транзакция. Коммит в конце метода. Приостановленная транзакция востонавливается после завершения основной. Используйте этот атрибут если хотите чтобы метод всегда выполнялся в новой транзакции.
|
Mandatory |
Если клиент выполняется внутри транзакции и вызывает метод корпоративного бина, метод выполняется в клиентской транзакции. Если клиент не ассоциирован с транзакцией, контейнер выбрасывает TransactionRequiredException. |
Supports |
Если клиент ассоциирован с какой-нибудь транзакцией, то метод будет выполняться внутри данной транзакции, если нет... ну её эту транзакцию, без нее все сделает. Атрибут как можно предположить весьма противоречивый и использовать его нужно с осторожностью. |
NotSupported |
Метод без транзакций. Если клиент ассоциирован с транзакцией то она приостанавливается на время выполнения метода. Если транзакций нет, то новая тоже не создается. Используется для методов не нуждающихся в транзакциях. Позволяет снизить нагрузку на сервер путем исключения транзакций там где она не нужна. |
Never |
Метод не терпящий транзакций не под каким видом. Если клиент при вызове метода ассоциирован с транзакцией, то выбрасывается исключение RemoteException . Метод выполнится без ошибок только при отсуствие каких либо транзакций у клиента.
|
Внути метода мы проверяем пароли на идентичность и после этого выполняем две функции по добавлению нового пользователя и его контактов (их реализация чуть ниже). В случае возникновения исключительной ситуации мы обрекаем весь метод на откат транзакции строкой context.setRollbackOnly();
- Теперь реализуем методы
newUser
и newContacts
. Можете воспользоваться помощью IDE и создать каркас методов. Теперь наполним их смыслом:
private Users newUser(String login, String password) {
Users user = new Users(login, password);
em.persist(user);
return user;
}
Из выше приведенного когда нас интересует только метод em.persist(user);
, именно он ставит созданный объект класса Users в очередь на добавление в БД, которое произойдет при следующем коммите. Не трудно догадаться что у нас он произойдет при завершение транзакции. Если выполнять этот метод с объектом уже имеющемся в БД то он вызовет исключение javax.persistence.PersistenceException
.
Если при работе понадобится вручную вызвать коммит то это делается командой:
em.flush();
еще раз замечу что в нашем случае это не нужно так как все операции выполняются в рамках объявленной транзакции под управлением CMP и по ее завершению коммит (или роллбэк) произойдет автоматически).
если вы получили некий объект из БД, произвели в нем изменения и хотите записать эти изменения в БД (UPDATE Statement), это делается функцией
em.merge(object);
так же надо сказать что эта функция может вести себя по разному: если например Вы выполните эту функцию с объектом которого еще нет в БД, то она автоматически его туда добавит (INSERT Statement).
Чтобы удалить объект из БД используется функция:
em.remove(object);
Обновление объекта из БД проиходит вызовом функции:
em.refresh(object);
- Метод
newContacts
:
private void newContacts(Users user, Map<String, String[]> contacts) {
if (contacts.size()>0){
for (Map.Entry<String, String[]> entry : contacts.entrySet()) {
String key = entry.getKey();
String[] values = entry.getValue();
for (String value : values) {
Contacts contact=new Contacts();
contact.setLogin(user);
contact.setName(key);
contact.setValue(value);
em.persist(contact);
}
}
}
}
Здесь мы проверяем что нам передали не пустой Map
, открываем цикл по каждому элементу и в нем же цикл параметров. На каждом из этапов создаем новый класс Conctacts
. Важно отметить что в метод setLogin
мы передаем созданный нами ранее экзепляр класса User
.
- Взглянув на реализованный нами метод
addUser
можно предвидеть исключительную ситуацию когда добавляется уже имеющийся узер (одинаковый login). Мы не можем использовать метод merge(user);
т.к. он просто обновит данные узера, а это нам совсем не к чему. Необходимо добавить проверку на уникальность логина.
Мы реализуем эту проверку путем запроса к БД который хранится в entity классе Users
. Напомни что при написании аннотации в entity классах есть следующие строки:
@NamedQueries({
@NamedQuery(name = "Users.findAll", query = "SELECT u FROM Users u"),
@NamedQuery(name = "Users.findByLogin", query = "SELECT u FROM Users u WHERE u.login = :login"),
@NamedQuery(name = "Users.findByPass", query = "SELECT u FROM Users u WHERE u.pass = :pass")})
Это проименованный список запросов в который, вы, можете добавлять свои. Нас интересует второй по счету запрос при котром параметром поиска является login пользователя.
Перепешите метод addUser
следующим образом:
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Integer addUser(final String login, final String password, final String passwordTwo, final Map<String, String[]> contacts) {
try{
if (login!=null && password!=null && passwordTwo!=null && password.equals(passwordTwo)){
List resultList = em.createNamedQuery("Users.findByLogin").setParameter("login", login).getResultList();
if (resultList.size()==0){
Users user=newUser(login, password);
newContacts(user,contacts);
return 0;
}else {return 3;}
}else {return 2;}
}catch (Exception e){
context.setRollbackOnly();
e.printStackTrace();
return 1;
}
}
Самая важная строка для нас это:
List resultList = em.createNamedQuery("Users.findByLogin").setParameter("login", login).getResultList();
В ней мы обращаемся к методу EntityManager'a createNamedQuery
передавая ему названия запроса, далее присваеваем значению параметру логина и получаем результат. Вот так просто мы выполнили запрос к БД.
- Дело за малым. Осталось должным образом обработать запрос на регистрацию в нашем сервлете. Откройте файл web_controller
- В начале класса объявите новый EJB ресурс:
@EJB
UsersManager userManager;
- Тело условного оператора который обрабатывает запрос от клиента по адресу /registration будет выглядеть следующим образом:
if ("/registration".equals(userPath)){
String login=null,pass=null,pass2=null;
HashMap<String, String[]> contacts =new HashMap<String, String[]>();
Enumeration<String> parameters = request.getParameterNames();
while (parameters.hasMoreElements()) {
String parameter = parameters.nextElement();
if (parameter.equals("login")){
login=request.getParameter(parameter);
}else
if (parameter.equals("password")){
pass=request.getParameter(parameter);
}else
if (parameter.equals("password2")){
pass2=request.getParameter(parameter);
}else{
contacts.put(parameter, request.getParameterValues(parameter));
}
}
Integer codeOperation=userManager.addUser(login, pass2, pass2, contacts);
if (codeOperation!=0)
{request.setAttribute("notif", "Код завершения операции: "+codeOperation);}
else
{request.setAttribute("notif", "Пользователь "+login+" успешно создан!");}
}
- В нем мы так же как и в прошлом обработчике перебираем список параметром от пользователя на наличие нужных нам после этого вызываем созданный нами бизнес метод
addUser
. Если код завершения операции равен 0 то все прошло успешно и мы сообщаем об этом с радостью нашему пользователю. Сообщение выводится в туже самую страницу регистрации, что конечно в реальном блоге было бы не правильно и для этого нужно создать отдельную страницу, но нам важно научиться пользоваться функционалом javaee, и потому этого будет вполне достаточно.
Комментарии
Contacts contact=new Contacts(); contact.setLogin(user);contact.setName(key); contact.setValue(value);
поÑÐµÐ¼Ñ id не Ñоздаем или он авÑомаÑиÑеÑки ÑоздаеÑÑÑ?