Click here to Skip to main content
15,878,959 members
Articles / Web Development / Spring

Java Spring MVC Single Page App with Upida/Jeneva (Backend)

Rate me:
Please Sign up or sign in to vote.
4.80/5 (7 votes)
7 Jan 2015CPOL9 min read 38.2K   455   11   9
Web development using JSON is simple

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):

Java
public class Client {

    private Integer id;
    private String name;
    private String lastname;
    private Integer age;
    private Set<Login> logins;

    /* getters and setters go here */
} 

public class Login {

    private Integer id;
    private String name;
    private String password;
    private Boolean enabled;
    private Client client;

    /* getters and setters go here */
}

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.

Java
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:

Java
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);
    }
 
    /* others basic methods */
}

I will have only one DAO class - ClientDao.

Java
@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).

Java
@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) {

        /* TODO: assign back-references of the child Login objects -
           for each Login: item.Login[i].Client = item; */
        this.clientDao.save(item);
    }

    @Override
    @Transactional
    public void update(Client item) {

        Client existing = this.clientDao.load(item.getId());
        /* TODO: copy changes from item to existing (recursively) */
        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:

Java
@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.

Java
@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.

Java
@Override
@Transactional
public void save(Client item) {

    /* TODO: assign back-references of the child Login objects -
       for each Login: item.Login[i].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.

Java
@Override
@Transactional
public void update(Client item) {

    Client existing = this.clientDao.load(item.getId());
    /* TODO: copy changes from item to existing (recursively) */
    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.

Java
@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.

Java
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.

Java
public class Client extends Dtobase {

    /* fields go here */

    @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 {

    /* fields go here */

    @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 Logins 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.

Java
@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.

Java
public class Login extends Dtobase implements IChild {

    private Integer id;
    private String name;
    private String password;
    private Boolean enabled;
    private Client client;

    /* getters and setters go here */
 
    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 nulls, and therefore you cannot rely on blind copy of all the fields, as you don't want nulls 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:

Java
@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.

XML
<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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionERROR: [SQLITE_ERROR] SQL error or missing database (no such table: Client) Pin
Alok Pal20-Jan-18 11:44
Alok Pal20-Jan-18 11:44 
QuestionProblem Pin
Member 1159131711-Apr-15 13:42
Member 1159131711-Apr-15 13:42 
AnswerRe: Problem Pin
vladimir husnullin15-Apr-15 3:12
vladimir husnullin15-Apr-15 3:12 
GeneralMy vote of 2 Pin
Member 112107805-Nov-14 21:45
Member 112107805-Nov-14 21:45 
GeneralRe: My vote of 2 Pin
vladimir husnullin6-Nov-14 23:08
vladimir husnullin6-Nov-14 23:08 
GeneralRe: My vote of 2 Pin
Member 112107807-Nov-14 1:18
Member 112107807-Nov-14 1:18 
GeneralRe: My vote of 2 Pin
vladimir husnullin7-Nov-14 5:24
vladimir husnullin7-Nov-14 5:24 
QuestionA little wonder Pin
Member 1109098517-Sep-14 21:12
Member 1109098517-Sep-14 21:12 
AnswerRe: A little wonder Pin
vladimir husnullin21-Sep-14 21:38
vladimir husnullin21-Sep-14 21:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.