Introduction
Let's try to create a simple web application using the most modern technologies and see what problems we may face. I will use the latest Spring MVC and latest Hibernate. I am going to use Spring Mvc JSON API to its full potential - i.e., all interactions between browser and server will go asynchronously in JSON. In order to accomplish this, I am going to use JavaScript library - AngularJS. But it is not a big deal if you prefer KnockoutJS or anything else.
Note, this article is about backend only. If you are curious about front-end side, then follow this link: Java Spring MVC Single Page App with Upida/Jeneva (Frontend/AngularJS).
Let's imagine we have a simple database with two tables: Client
and Login
, every client can have one to many logins. My application will have three pages - "list of clients", "create client", and "edit client". The "create client" and the "edit client" pages will be capable of editing client data as well as managing the list of child logins. Here is the link to the resultant web application.
First of all, let's define the domain (or model) classes (mapping is defined in hbm files):
public class Client {
private Integer id;
private String name;
private String lastname;
private Integer age;
private Set<Login> logins;
}
public class Login {
private Integer id;
private String name;
private String password;
private Boolean enabled;
private Client client;
}
Now, I can create Data-Access layer. First of all, I must have base DAO (and interface) class which is injected with Hibernate SessionFactory
, and defines basic DAO operations: Save
, Delete
, Update
, Load
, Get
, etc.
public interface IDaobase<T> {
void save(T item);
void update(T item);
T merge(T item);
void delete(T item);
T get(Serializable id);
T load(Serializable id);
}
And here is the Daobase
class:
public class Daobase<T> implements IDaobase<T> {
protected SessionFactory sessionFactory;
public Daobase(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public void save(T entity) {
this.sessionFactory
.getCurrentSession
.save(entity);
}
@Override
public void update(T entity) {
this.sessionFactory
.getCurrentSession
.update(entity);
}
}
I will have only one DAO class - ClientDao
.
@Repository
public class ClientDao extends Daobase<Client> implements IClientDao {
@Autowired
public ClientDao (SessionFactory sessionFactory) {
super(sessionFactory);
}
@Override
public Client getById(int id) {
return (Client)this.sessionFactory
.getCurrentSession()
.createQuery("from Client client left outer
join fetch client.logins where client.id = :id");
.setParameter("id", id);
.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
.uniqueResult();
}
@Override
public List<Client> GetAll() {
return this.sessionFactory
.getCurrentSession()
.createQuery("from Client client left outer join fetch client.logins");
.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
.list();
}
}
When DAO is done, we can switch to Service layer. Services are usually responsible for opening and closing transactions. I have only one service class. It is injected with respected DAO class.
Note, the save()
and update()
methods accept a Client
object and its child Logins
, and therefore perform save or update operations, using Hibernate cascading (persisting parent and children at the same time).
@Service
public class ClientService implements IClientService {
private IClientDao clientDao;
public ClientService(IClientDao clientDao) {
this.clientDao = clientDao;
}
@Override
@Transactional(readOnly=true)
public Client getById(int clientId) {
Client item = this.clientDao.GetById(clientId);
return item;
}
@Override
@Transactional(readOnly=true)
public List<Client> getAll() {
List<Client> items = this.clientDao.getAll();
return items;
}
@Override
@Transactional
public void save(Client item) {
this.clientDao.save(item);
}
@Override
@Transactional
public void update(Client item) {
Client existing = this.clientDao.load(item.getId());
this.clientDao.merge(existing);
}
}
Let's talk a bit about controller. I am going to have a controller
class which has two responsibilities. Firstly, it maps incoming URL text to the corresponding HTML view. And secondly, it is responsible to handle REST service calls from JavaScript. Here is how it looks:
@Controller
@RequestMapping({"/client"})
public class ClientController {
private IClientService clientService;
@Autowired
public ClientController(IClientService clientService) {
this.clientService = clientService;
}
@RequestMapping(value={"/list"})
public String list() {
return "client/list";
}
@RequestMapping("/create")
public String create() {
return "client/create";
}
@RequestMapping("/edit")
public String edit() {
return "client/edit";
}
@RequestMapping("/getbyid")
@ResponseBody
public Client getById(int id) {
return this.clientService.getById(id);
}
@RequestMapping("/getall")
@ResponseBody
public List<Client> getAll() {
return this.clientService.getAll();
}
@RequestMapping("/save")
@ResponseBody
public void save(@RequestBody Client item) {
this.clientService.save(item);
}
@RequestMapping("/update")
@ResponseBody
public void update(@RequestBody Client item) {
this.clientService.update(item);
}
}
The first three methods: list()
, create()
, edit()
just return HTML view name. The other methods are more complicated, they represent REST service, exposed to JavaScript.
Now, we have almost everything we need. The MVC controller will give us HTML and JavaScript, which will interact asynchronously with the API controller and fetch data from database. AngularJS will help us to display the fetched data as beautiful HTML. We are not going to talk about HTML and JavaScript in this article. I assume that you are familiar with AngularJS (or KnockoutJS), though it is not that important in this article. The only thing you must know is - every page is loaded as static HTML and JavaScript, after being loaded, it interacts with controller to load all the needed data pieces from database through JSON, asynchronously. And AngularJS helps to display the JSON as beautiful HTML.
Problems
Now, let's talk about problems that we face in the current implementation.
Problem 1
The first problem is serialization. Data, returned from the controller is serialized to JSON. You can see it in these two controller methods.
@Controller
@RequestMapping({"/client"})
public class ClientController {
....
@RequestMapping("/getbyid")
@ResponseBody
public Client getById(int id) {
return this.clientService.getById(id);
}
@RequestMapping("/getall")
@ResponseBody
public List<Client> getAll() {
return this.clientService.getAll();
}
The Client
class is a domain class, and it is wrapped with Hibernate wrapper. So, serializing it can result in circular dependency and will cause StackOverflowException
. But there are other minor concerns. For example, sometimes, I need only id
and name
fields to be present in JSON, sometimes I need all the fields (same objects must be serialized differently - including different sets of fields). The current implementation does not allow me to make that decision, it will always serialize all fields.
Problem 2
If you take a look at the ClientService
class, method save()
, you will see that there is some code missing.
@Override
@Transactional
public void save(Client item) {
this.clientDao.save(item);
}
Which means, that before saving the Client
object, you have to set up back-references of the children Login
objects. Every Login
class has a field - Client
, which is actually a back-reference to the parent Client
object. So, in order to save Client
with Logins
together using cascading save, you have to set up those fields to the actual parent instance. When Client
is deserialized from JSON, it does not have back-references. It is a well-known problem among Hibernate users.
Problem 3
If you take a look at the ClientService
class, method update()
, you will see that there is some code missing too.
@Override
@Transactional
public void update(Client item) {
Client existing = this.clientDao.load(item.getId());
this.clientDao.merge(existing);
}
I also have to implement logic, which copies the fields from the deserialized Client
object to the existing persistent instance of the same Client
. My code must be smart enough to go through the children Logins
. It must match the existing logins with the deserialized ones, and copy fields respectively. It must also append newly added Logins
, and delete missing ones. After these modifications, the Merge()
method will persist all the changes to database. So this is quite sophisticated logic.
In the next section, we will solve these three problems using Jeneva.
Solution
Problem 1 - Smart Serialization
Let's see how Jeneva can help us to solve the first problem. The ClientController
has two methods that return Client
objects - getAll()
and getById()
. The getAll()
method returns list of Clients
, which is displayed as grid. I don't need all the fields of the Client
object to be present in JSON. The GetById()
method is used on the "Edit Client" page. Therefore full Client
information is required here.
In order to solve this problem, I have to go through each property of the returned objects, and assign null
value to every property that I don't need. This seems pretty hard work, because I have to do it in every method differently. Jeneva provides us with org.jeneva.Mapper
class which can do it for us. Let's modify the business layer using the Mapper
class.
@Service
public class ClientService extends IClientService {
private IMapper mapper;
private IClientDao clientDao;
@Autowired
public ClientService(IMapper mapper, ClientDao clientDao) {
this.mapper = mapper;
this.clientDao = clientDao;
}
@Override
public Client getById(int clientId) {
Client item = this.clientDao.getById(clientId);
return this.mapper.filter(item, Leves.DEEP);
}
@Override
public List<Client> getAll() {
List<Client> items = this.clientDao.getAll();
return this.mapper.filterList(items, Levels.GRID);
}
.....
It looks very simple, Mapper
takes the target object or list of objects and produces a copy of them, but every unneeded property is set to null
. The second parameter is a numeric value which represents the level of serialization. Jeneva does not comes with default levels, you must define your own.
public class Levels {
public static final byte ID = 1;
public static final byte LOOKUP = 2;
public static final byte GRID = 3;
public static final byte DEEP = 4;
public static final byte NEVER = 100;
}
The last step is to decorate every property of my domain classes with corresponding level. I am going to use DtoAttribute
from Jeneva to decorate the <code>Client
and Login
class properties.
public class Client extends Dtobase {
@Dto(Levels.ID)
public Integer getId() { return this.id; }
@Dto(Levels.LOOKUP)
public String getName() { return this.name; }
@Dto(Levels.GRID)
public String getLastname() { return this.lastname; }
@Dto(Levels.GRID)
public Integer getAge() { return this.age; }
@Dto(Levels.GRID, Levels.LOOKUP)
public ISet<Login> getLogins() { return this.logins; }
}
public class Login extends Dtobase {
@Dto(Levels.ID)
public Integer getId() { return this.id; }
@Dto(Levels.LOOKUP)
public String getName() { return this.name; }
@Dto(Levels.GRID)
public String getPassword() { return this.password; }
@Dto(Levels.GRID)
public Boolean getEnabled() { return this.enabled; }
@Dto(Levels.NEVER)
public Client getClient() { return this.client; }
}
After all properties are decorated, I can use Mapper
class. For example, if I call Mapper.filter()
method with Levels.ID
, then only properties marked with ID
will be included. If I call Mapper.filter()
method with Levels.LOOKUP
, than properties marked with ID
and LOOKUP
will be included, because ID
is less than LOOKUP
(10 < 20). Take a look at the Client.logins
property, as you see there are two levels applied there, what does it mean? It means that if you call Mapper.filter()
method with Levels.GRID
, then logins will be included, but LOOKUP
level will be applied to the properties of the Login
class. And if you call Mapper.filter()
method with level higher than GRID
, then the level applied to the Login
properties will become respectively higher.
Problem 2 - Back-references
Take a look at the business layer class, save()
method. As you see, this method accepts Client
object. I use cascading save - I save Client
and its Login
s together. In order to accomplish this, the children Login
objects must have back-reference assigned correctly to the parent Client
object. Basically, I have to loop through children Logins
and assign Login.client
property to the root Client
. When this is done, I can save the Client
object using Hibernate tools.
Instead of writing a loop, I am going to use org.jeneva.Mapper
class again. Let's modify the ClientService
class.
@Service
public class ClientService implements IClientService {
private IMapper mapper;
private IClientDao clientDao;
@Autowired
public ClientService(IMapper mapper, ClientDao clientDao) {
this.mapper = mapper;
this.clientDao = clientDao;
}
....
@Override
public void save(Client item) {
this.mapper.map(item);
this.clientDao.save(item);
}
This code will recursively go through properties of the Client
object and set up all the back-references. This is actually half of the solution, another half goes in this code. Every child class must implement IChild
interface, where it can tell about who his parent is. The connectToParrent()
method will be called internally by Mapper
class. The Mapper
will suggest possible parents based on JSON.
public class Login extends Dtobase implements IChild {
private Integer id;
private String name;
private String password;
private Boolean enabled;
private Client client;
public void connectToParent(Object parent) {
if(parent instanceof Client) {
this.Client = (Client)parent;
}
}
}
If IChild
interface is implemented correctly, you only have to call Map()
method from your business layer, and all back-references will be assigned correctly.
Problem 3 - Mapping Updates
The third problem is the most complicated, because updating client is a complicated process. In my case, I have to update client fields as well as update children logins' fields, and at the same time I have to append, delete children logins if user has deleted or inserted new logins. By the way, updating any object, even if you are not using cascading updates, is complicated. Mostly, because when you want to update an object, you always have to write custom code to copy changes from incoming object to existing one. Usually, the incoming object contains just several important fields to update, the rest are null
s, and therefore you cannot rely on blind copy of all the fields, as you don't want null
s to be copied to existing data.
The Mapper
class can copy changes from the incoming object to the persistent one, without overwriting any important fields. How does it work? Jeneva comes with a JenevaJsonConverter
class, which derives from the MappingJacksonHttpMessageConverter
used in Spring MVC by default. JenevaJsonConverter
contains some minor adjustments. As you know, every domain class derives from org.jeneva.Dtobase
abstract class. This class contains HashSet
of property names. When JenevaJsonConverter
parses JSON, it passes the information about parsed fields to Dtobase
, and Dtobase
object remembers which of the fields are assigned. Therefore, every domain object knows which of the fields are assigned during JSON parsing. Later, Mapper
class goes through only assigned properties of the incoming deserialized object and copies their values to the existing persistent object.
Here is the business layer update()
method using the Mapper
class:
@Service
public class ClientService {
private IMapper mapper;
private IClientDao clientDao;
@Autowired
public ClientService(IMapper mapper, ClientDao clientDao) {
this.mapper = mapper;
this.clientDao = clientDao;
}
....
@Override
public void update(Client item) {
Client existing = this.clientDao.load(item.getId());
this.mapper.mapTo(item, existing, Client.class);
this.clientDao.merge(existing);
}
}
And here is part of the spring beans file. You can see how to set up JenevaJsonConverter
to be default converter in your web application. Please, don't worry about switching from Spring default converter. If you take a look at Jeneva converter, it derives from Jackson converter and provides just minor changes.
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.jeneva.spring.JenevaJsonConverter" />
</mvc:message-converters>
</mvc:annotation-driven>
Notes
Solving the above-mentioned problems is the biggest side of what Jeneva can do. However, there is another interesting feature, that can help you in implementing validation routines - both server-side and client-side.
You can find out more details on how to implement validation using Jeneva in this article: Validating incoming JSON using Upida/Jeneva.
And also, you can find out how to create Single-Page web Application (SPA) using AngularJS in my next article: AngularJS single-page app and Upida/Jeneva.
References