Monday, January 23, 2006

Examples strike back :-)

Ok, here we go. First of all, prerequisition:

  • EJB3 container (glassfish is used in this scenario)
  • Database (Oracle XE with activated HR scheme)
  • Ant
  • A lot of time :-)
    If you've downloaded all the software required, you can move on to second stage. First of all let's create entity beans. These're plain old java objects (POJO) with some annotations. I've created two version. One that contain full annotation, including relationships and the second one (with prefix Bare) that is relationshipless. Here is snapshot of class Employees with full links to other tables:

    package org.defectus.ejb3.ic2.entity.complex;

    import java.io.Serializable;
    import java.util.Collection;

    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.NamedQuery;
    import javax.persistence.OneToMany;
    import javax.persistence.Table;

    @Entity
    @Table(name = "EMPLOYEES")
    @NamedQuery(name = "selectAllEmps", query = "select object(o) from Employees o")
    public class Employees implements Serializable {
    private static final long serialVersionUID = 1987305452306161213L;

    private Double commissionPct;

    private String email;

    private Long employeeId;

    private String firstName;

    private String lastName;

    private String phoneNumber;

    private Double salary;

    private Departments department;

    private Employees manager;

    private Collection employees;

    private Jobs jobs;

    private Collection departments;

    public Employees() {
    }

    public Employees(String email, Long employeeId, Jobs jobs, String lastName) {
    this.email = email;
    this.employeeId = employeeId;
    this.jobs = jobs;
    this.lastName = lastName;
    }

    @Column(name = "COMMISSION_PCT")
    public Double getCommissionPct() {
    return commissionPct;
    }

    public void setCommissionPct(Double commissionPct) {
    this.commissionPct = commissionPct;
    }

    @Column(name = "EMAIL", nullable = false)
    public String getEmail() {
    return email;
    }

    public void setEmail(String email) {
    this.email = email;
    }

    @Id()
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "EMPLOYEES_SEQ")
    @Column(name = "EMPLOYEE_ID", nullable = false)
    public Long getEmployeeId() {
    return employeeId;
    }

    public void setEmployeeId(Long employeeId) {
    this.employeeId = employeeId;
    }

    @Column(name = "FIRST_NAME")
    public String getFirstName() {
    return firstName;
    }

    public void setFirstName(String firstName) {
    this.firstName = firstName;
    }

    @Column(name = "LAST_NAME", nullable = false)
    public String getLastName() {
    return lastName;
    }

    public void setLastName(String lastName) {
    this.lastName = lastName;
    }

    @Column(name = "PHONE_NUMBER")
    public String getPhoneNumber() {
    return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
    this.phoneNumber = phoneNumber;
    }

    @Column(name = "SALARY")
    public Double getSalary() {
    return salary;
    }

    public void setSalary(Double salary) {
    this.salary = salary;
    }

    @ManyToOne()
    @JoinColumn(name = "DEPARTMENT_ID", referencedColumnName = "DEPARTMENTS.DEPARTMENT_ID")
    public Departments getDepartment() {
    return department;
    }

    public void setDepartment(Departments departments) {
    this.department = departments;
    }

    @ManyToOne()
    @JoinColumn(name = "MANAGER_ID", referencedColumnName = "EMPLOYEES.EMPLOYEE_ID")
    public Employees getManager() {
    return manager;
    }

    public void setManager(Employees manager) {
    this.manager = manager;
    }

    @OneToMany()
    public Collection getEmployees() {
    return employees;
    }

    public void setEmployees(Collection employees) {
    this.employees = employees;
    }

    public Employees addToEmployeesCollection(Employees employees) {
    getEmployees().add(employees);
    employees.setManager(this);
    return employees;
    }

    public Employees removeFromEmployeesCollection(Employees employees) {
    getEmployees().remove(employees);
    employees.setEmployees(null);
    return employees;
    }

    @ManyToOne()
    @JoinColumn(name = "JOB_ID", referencedColumnName = "JOBS.JOB_ID")
    public Jobs getJobs() {
    return jobs;
    }

    public void setJobs(Jobs jobs) {
    this.jobs = jobs;
    }

    @OneToMany()
    public Collection getDepartments() {
    return departments;
    }

    public void setDepartments(Collection departments) {
    this.departments = departments;
    }

    public Departments addToDepartmentsCollection(Departments departments) {
    getDepartments().add(departments);
    departments.setManager(this);
    return departments;
    }

    public Departments removeFromDepartmentsCollection(Departments departments) {
    getDepartments().remove(departments);
    departments.setManager(null);
    return departments;
    }
    }
    As you can see, it's very straightward - nothing is hidden and, there is no logic ! No methods, except for constructor. But if you take a closer look on OneToMany mapping you can realize that there must be some kind of executive code out there. And it certainly is. Entity manager hides further logic behind the Collection of Departments. This logic is responsible for lazy fetching of Departments. As long as you run this code inside the container (ie. Glassfish) everything is all right but once you try to send such POJO over let's say webservice you can encounter a serious problem. As far as I'm aware there's no way how to detach such POJO and attached POJO can't be detached by SOAP message creator. And that's the reason why I have created second version of EJBs. In the next example you'll see the same table (Employees) but without any relationship included:

    package org.defectus.ejb3.ic2.entity.bare;

    import java.io.Serializable;

    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.EntityResult;
    import javax.persistence.FieldResult;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.NamedNativeQuery;
    import javax.persistence.NamedQuery;
    import javax.persistence.SqlResultSetMapping;
    import javax.persistence.Table;

    @Entity
    @Table(name = "EMPLOYEES")
    @NamedQuery(name = "selectAllEmps", query = "select object(o) from Employees o")
    @NamedNativeQuery(name = "selectNativeEmps", query = "SELECT EMPLOYEE_ID, FIRST_NAME, LAST_NAME, SALARY FROM EMPLOYEES WHERE EMPLOYEE_ID = ?", resultSetMapping = "employeeNamedMapping", resultClass=org.defectus.ejb3.ic2.entity.bare.BareEmployees.class)
    @SqlResultSetMapping(name = "employeeNamedMapping", entities = { @EntityResult(entityClass = org.defectus.ejb3.ic2.entity.bare.BareEmployees.class, fields = {
    @FieldResult(name = "employeeId", column = "EMPLOYEE_ID"), @FieldResult(name = "firstName", column = "FIRST_NAME"),
    @FieldResult(name = "lastName", column = "LAST_NAME"), @FieldResult(name = "salary", column = "SALARY") }) /*, discriminatorColumn="disc")*/})
    public class BareEmployees implements Serializable {
    private static final long serialVersionUID = 1987305452306161213L;

    private Double commissionPct;

    private String email;

    private Long employeeId;

    private String firstName;


    private String lastName;

    private String phoneNumber;

    private Double salary;

    private Long departments;

    private Long employees;

    private String jobs;

    public BareEmployees() {
    }

    public BareEmployees(String email, Long employeeId, String jobs, String lastName) {
    this.email = email;
    this.employeeId = employeeId;
    this.jobs = jobs;
    this.lastName = lastName;
    }

    @Column(name = "COMMISSION_PCT")
    public Double getCommissionPct() {
    return commissionPct;
    }

    public void setCommissionPct(Double commissionPct) {
    this.commissionPct = commissionPct;
    }

    @Column(name = "EMAIL", nullable = false)
    public String getEmail() {
    return email;
    }

    public void setEmail(String email) {
    this.email = email;
    }

    @Id()
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "EMPLOYEES_SEQ")
    @Column(name = "EMPLOYEE_ID", nullable = false)
    public Long getEmployeeId() {
    return employeeId;
    }

    public void setEmployeeId(Long employeeId) {
    this.employeeId = employeeId;
    }

    @Column(name = "FIRST_NAME")
    public String getFirstName() {
    return firstName;
    }

    public void setFirstName(String firstName) {
    this.firstName = firstName;
    }

    @Column(name = "LAST_NAME", nullable = false)
    public String getLastName() {
    return lastName;
    }

    public void setLastName(String lastName) {
    this.lastName = lastName;
    }

    @Column(name = "PHONE_NUMBER")
    public String getPhoneNumber() {
    return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
    this.phoneNumber = phoneNumber;
    }

    @Column(name = "SALARY")
    public Double getSalary() {
    return salary;
    }

    public void setSalary(Double salary) {
    this.salary = salary;
    }

    @Column(name = "DEPARTMENT_ID")
    public Long getDepartments() {
    return departments;
    }

    public void setDepartments(Long departments) {
    this.departments = departments;
    }

    @Column(name = "MANAGER_ID")
    public Long getEmployees() {
    return employees;
    }

    public void setEmployees(Long employees) {
    this.employees = employees;
    }

    @Column(name = "EMPLOYEES.JOB_ID")
    public String getJobs() {
    return jobs;
    }

    public void setJobs(String jobs) {
    this.jobs = jobs;
    }
    }
    In this example you perhaps lack the relationship annotations and structures but a native query has been add. Note that EJB3 easily fall to annotation defaulting. This means that even if you omit annotate class field as Column deployer will find that in related table is matching column and will bind tthe column and the filed together.
    And now for something completely different :-) Let's take look on a facade class. Its purpose is to create an interface and cover all the logic behind. There're some important things you should be noticed about. Class is annotated as Stateless - it means that class is stateless session bean. Also, class has interceptor. This feature will be discussed later. In a body of the class is something called EntityManager annotated with @PersistenceContext. It indicates that class uses persistence and container should inject EntityManager to given attribute. Note that this way of instructing container to inject EntityManager is not the only one way how to do it and the other ways are perhaps even better (consider for example usage of EntityManagerFactory). pu1 specified which persitence context should be used. Later in the code you'll see some methods, which, as I hope, should be easy to understand.

    package org.defectus.ejb3.ic2.session;

    import java.sql.Timestamp;
    import java.util.Date;
    import java.util.List;

    import javax.ejb.Interceptors;
    import javax.ejb.Stateless;
    import javax.persistence.EntityManager;
    import javax.persistence.PersistenceContext;

    import org.defectus.ejb3.ic2.entity.bare.BareDepartments;
    import org.defectus.ejb3.ic2.entity.bare.BareEmployees;
    import org.defectus.ejb3.ic2.entity.bare.BareJobs;
    import org.defectus.ejb3.ic2.entity.bare.BareLogs;
    import org.defectus.ejb3.ic2.entity.complex.Departments;
    import org.defectus.ejb3.ic2.entity.complex.Employees;
    import org.defectus.ejb3.ic2.entity.complex.Locations;

    @Stateless (mappedName="ejb/DataFacade")
    @Interceptors( { org.defectus.ejb3.ic2.session.DataInterceptor.class })
    public class DataFacadeBean implements DataFacade {
    @PersistenceContext(unitName = "pu1")
    private EntityManager _entityManager;

    public DataFacadeBean() {
    }

    public EntityManager getEntityManager() {
    return _entityManager;
    }

    public void setEntityManager(EntityManager entityManager) {
    _entityManager = entityManager;
    }

    public List findDept(String name) {
    @SuppressWarnings(value = { "unchecked" })
    List deps = getEntityManager().createQuery(
    "select object(o) from Departments o where o.departmentName=:name").setParameter("name", name).getResultList();
    return deps;
    }

    public List findAllDept() {
    @SuppressWarnings(value = { "unchecked" })
    List deps = getEntityManager().createQuery("select object(o) from Departments o").getResultList();
    return deps;
    }

    public Departments createDepartment(String departmentName, Long locationId) {
    final Departments dept = new Departments(departmentName);
    Locations location = getEntityManager().find(Locations.class, locationId);
    dept.setLocation(location);
    getEntityManager().persist(dept);
    return dept;
    }

    public BareLogs createLog(String message) {
    final BareLogs log = new BareLogs(message, new Date());
    getEntityManager().persist(log);
    return log;
    }

    public List findAllComplexEmployees() {
    @SuppressWarnings(value = { "unchecked" })
    List emps = getEntityManager().createNamedQuery("selectAllEmps").getResultList();
    return emps;
    }

    public List findAllBareEmployees() {
    @SuppressWarnings(value = { "unchecked" })
    List emps = getEntityManager().createQuery("select object(o) from BareEmployees o").getResultList();
    return emps;
    }

    public List findBareEmployees2() {
    @SuppressWarnings(value = { "unchecked" })
    List emp = getEntityManager().createNamedQuery("selectNativeEmps").setParameter(1,101).getResultList();
    return emp;
    }

    public BareJobs findBareJobs(String jobId) {
    return getEntityManager().find(BareJobs.class, jobId);
    }

    public BareEmployees findBareEmployee(Long emplId) {
    return getEntityManager().find(BareEmployees.class, emplId);
    }

    public BareDepartments findBareDepartment(Long depId) {
    return getEntityManager().find(BareDepartments.class, depId);
    }

    public List findAllDepartments() {
    @SuppressWarnings(value = { "unchecked" })
    List deps = getEntityManager().createNamedQuery("selectAllDeps").getResultList();
    return deps;
    }

    public Employees createEmployee(Double comm, Long deptno, String email, String firstName, String lastName,
    Timestamp hiredate, String job, Double sal) {
    final Employees emp = new Employees();
    Departments dep = getEntityManager().find(Departments.class, deptno);
    emp.setCommissionPct(comm);
    emp.setDepartment(dep);
    emp.setEmail(email);
    emp.setFirstName(firstName);
    emp.setLastName(lastName);
    emp.setSalary(sal);
    getEntityManager().persist(emp);
    return emp;
    }

    public void updateEntity(Object entity) {
    getEntityManager().merge(entity);
    }

    public void deleteEntity(Object entity) {
    final EntityManager em = getEntityManager();
    em.remove(em.merge(entity)); // interesting way how to delete entity of any type
    }

    public void refreshEntity(Object entity) {
    getEntityManager().refresh(entity);
    }

    public void shutdown() {
    getEntityManager().close();
    }


    }
    For the bean a remote interface is needed. To create one you have to annotate appropriate interface as @Remote and make the class above implement such interface. Note that the interface can be Remote, Local but can't be both.

    package org.defectus.ejb3.ic2.session;

    import java.sql.Timestamp;
    import java.util.List;

    import javax.ejb.Remote;
    import javax.persistence.EntityManager;

    import org.defectus.ejb3.ic2.entity.bare.BareEmployees;
    import org.defectus.ejb3.ic2.entity.bare.BareJobs;
    import org.defectus.ejb3.ic2.entity.bare.BareLogs;
    import org.defectus.ejb3.ic2.entity.complex.Departments;
    import org.defectus.ejb3.ic2.entity.complex.Employees;

    @Remote
    public interface DataFacade {

    public abstract EntityManager getEntityManager();

    public abstract void setEntityManager(EntityManager entityManager);

    public abstract List findAllDept();

    public abstract List findDept(String name);

    public abstract Departments createDepartment(String departmentName, Long locationId);

    public abstract BareLogs createLog(String message);

    public abstract List findAllBareEmployees();

    public abstract BareJobs findBareJobs(String jobId);

    public abstract BareEmployees findBareEmployee(Long emplId);

    public abstract List findAllComplexEmployees();

    public abstract List findAllDepartments();

    public abstract Employees createEmployee(Double comm, Long deptno, String email, String firstName, String lastName,
    Timestamp hiredate, String job, Double sal);

    public abstract void updateEntity(Object entity);

    public abstract void deleteEntity(Object entity);

    public abstract void refreshEntity(Object entity);

    public abstract void shutdown();

    }
    Interceptor is something that is called before any particular call to method of intercepted bean. From the code below it's easy to understand the way of interception

    package org.defectus.ejb3.ic2.session;

    import javax.ejb.AroundInvoke;
    import javax.ejb.InvocationContext;

    import org.defectus.ejb3.ic2.entity.bare.BareEmployees;

    public class DataInterceptor {

    @AroundInvoke()
    public Object myBeanInterceptor(InvocationContext ctx) throws Exception {
    if (ctx.getMethod().getName().equals("findDept")) {
    String name = (String) ctx.getParameters()[0];
    Object[] o = new Object[1];
    o[0] = name;
    ctx.setParameters(o);
    } else if (ctx.getMethod().getName().equals("findBareEmployee")) {
    Long id = (Long) ctx.getParameters()[0];
    if (id.longValue() == 888)
    return new BareEmployees(null, null, null, "Who wants to know ? :-)");
    }
    return ctx.proceed();
    }
    }
    The second IF is interesting - because it interrupts call to underlaying method so the method is actually not called at all ! This means - when you ask the bean for employee 888 interceptor returns one without even calling datafacade bean. Very nice. Note that specification have changed and Intercaption resides in different package (but it works the same way as before).In the next class you'll see is a servlet that acts as data provider to AJAX page. Although servlets are not annotated (you must provide valid web.xml) you can use annotations though. Annotation @EJB instructs server to inject reference to EJB. The rest of the class is servlet as usual. Just for your information, the last Java EE 5 specification prohibit usage of stateful bean in servlets.

    package org.defectus.ejb3.ic2.ws;

    import java.io.IOException;
    import java.util.Map;

    import javax.ejb.EJB;
    import javax.servlet.ServletException;
    import javax.servlet.ServletOutputStream;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;

    import org.defectus.ejb3.ic2.entity.bare.BareEmployees;
    import org.defectus.ejb3.ic2.session.DataFacade;

    public class DataProvider extends HttpServlet {
    private static final long serialVersionUID = -7542313195870441831L;

    @EJB(name = "org.defectus.ejb3.ic2.session.DataFacade")
    private DataFacade data;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
    ServletOutputStream o = res.getOutputStream();
    res.setContentType("text/xml");
    res.setCharacterEncoding("utf-8");
    String action = null;
    Long id = null;
    try {
    action = req.getParameter("action");
    } catch (Exception e) {
    o.println("");
    o.println("");
    o.println("
    Invalid request !!! No action provided
    ");
    o.println("
    ");
    o.println("
    ");
    o.flush();
    o.close();
    return;
    }
    Map params = req.getParameterMap();
    if (action.equals("singleEmployee")) {
    printSingleEmployee(o, Long.getLong((String) params.get("id")));
    } else if (action.equals("employeeList")) {
    printEmployeeList(o);
    }
    }

    private void printSingleEmployee(ServletOutputStream o, Long value) throws IOException {
    o.println("");
    o.println("");
    o.println("
    " + data.findBareEmployee(value).getLastName() + "
    ");
    o.println("
    ");
    o.println("
    ");
    o.flush();
    o.close();
    }

    private void printEmployeeList(ServletOutputStream o) throws IOException {
    o.println("");
    o.println("");
    o.println("");
    o.println("
    ");
    o.println("
    ");
    o.flush();
    o.close();
    }
    }
    And that's all for now. I'll continue later and try to cover another techniques related to EJB 3.
  • 1 comment:

    Stephen Connolly said...

    I am curious as to why you have the two sets of classes: e.g. Employee and BareEmployee

    It seems like a lot of duplicate work generating both sets, what's wrong with FetchType.LAZY?

    Or is this the solution I have been looking for?

    The problem I see is when passing not Bare objects to other layers you could very quickly end up passing the whole database if your database is any way well connected. The solution I keep on hitting is to write DTOs, which feels wrong. I tried having each @Entity have it's own clone(int level) not quite deep copy function which replaces all collections with ArrayLists and having the @Stateless use that, and at least that removed the extra class... but we're still doing the DTO and we still need to write synchronisation code. em.detach(object) doesn't provide a way to know how much of the FetchType.LAZY we have in the object and short of having each @Entity pull in all the LAZY properties in a traverseChildren(int level) I'm still not seeing eligant solutions.

    So, yeah, why the two sets of classes?