Refactoring the Java classes
The classes generated by the reverse engineering process can be improved upon with a little refactoring to make the code more readable and easier to understand. Some of the autogenerated properties and fields have id
in their name when we are actually referring to classes, while the collection of java.util.List
objects have list
in their name. Let's start with the Company.java
file.
The Company.java file
This file represents the Company
entity. Double-click on the file to open it in the editor and browse through the contents. This class is a simple POJO with set and get methods for each property in addition to the standard hashCode
, equals
, and toString
methods. The class has a no-arg constructor (required by the JPA specification as domain objects must be created dynamically without any properties), a second constructor that takes only the primary key, and a full (all arguments) constructor. We will make the code more readable by making a few minor changes to the Company.java
file.
The first change is to rename the field projectList
to projects
everywhere in the file. This can be easily achieved by selecting the projectList
field, and then selecting Refactor | Rename from the menu:
You can now change the field name to projects. Make sure that you also select the Rename Getters and Setters option before clicking on the Refactor button.
Making these changes will change the field name and generate new get and set methods for the projects
field.
The final change for the Company.java
file is renaming the mappedBy
property from idCompany
to company
. The appropriate lines should now look like the following code:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "company") private List<Project> projects;
The final refactored Company.java
file should now look like the following code snippet:
package com.gieman.tttracker.domain; import java.io.Serializable; import java.util.List; import javax.persistence.Basic; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.OneToMany; import javax.persistence.Table; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @Entity @Table(name = "ttt_company") @NamedQueries({ @NamedQuery(name = "Company.findAll", query = "SELECT c FROM Company c"), @NamedQuery(name = "Company.findByIdCompany", query = "SELECT c FROM Company c WHERE c.idCompany = :idCompany"), @NamedQuery(name = "Company.findByCompanyName", query = "SELECT c FROM Company c WHERE c.companyName = :companyName")}) public class Company implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Basic(optional = false) @Column(name = "id_company") private Integer idCompany; @Basic(optional = false) @NotNull @Size(min = 1, max = 200) @Column(name = "company_name") private String companyName; @OneToMany(cascade = CascadeType.ALL, mappedBy = "company") private List<Project> projects; public Company() { } public Company(Integer idCompany) { this.idCompany = idCompany; } public Company(Integer idCompany, String companyName) { this.idCompany = idCompany; this.companyName = companyName; } public Integer getIdCompany() { return idCompany; } public void setIdCompany(Integer idCompany) { this.idCompany = idCompany; } public String getCompanyName() { return companyName; } public void setCompanyName(String companyName) { this.companyName = companyName; } public List<Project> getProjects() { return projects; } public void setProjects(List<Project> projects) { this.projects = projects; } @Override public int hashCode() { int hash = 0; hash += (idCompany != null ? idCompany.hashCode() : 0); return hash; } @Override public boolean equals(Object object) { if (!(object instanceof Company)) { return false; } Company other = (Company) object; if ((this.idCompany == null && other.idCompany != null) || (this.idCompany != null && !this.idCompany.equals(other.idCompany))) { return false; } return true; } @Override public String toString() { return "com.gieman.tttracker.domain.Company[ idCompany=" + idCompany + " ]"; } }
JPA uses the convention-over-configuration concept to simplify the configuration of entities. This is achieved by using annotations with sensible defaults to keep the entity definitions lean. Now, let's look at the key JPA annotations in this file.
This is a marker annotation that indicates to the JPA persistence provider that the Company
class is an entity. JPA scans for the @Entity
annotations when exclude-unlisted-classes
is set to false
in the persistence.xml
file. Without the @Entity
annotation, the persistence engine will ignore the class.
The @Table
annotation defines the underlying database table that is represented by this entity class. The @Table(name = "ttt_company")
line tells the persistence provider that the Company
class represents the ttt_company
table. Only one table annotation can be defined in any entity class.
The @Id
annotation defines the primary key field in the class and is required for each entity. The persistence provider will throw an exception if the @Id
annotation is not present. The Company
class property representing the primary key in the ttt_company
table is the Integer idCompany
field. There are three additional annotations attached to this field, of which the following annotation is specific to primary keys.
This annotation identifies how the persistence engine should generate new primary key values for the insertion of records into the table. The strategy=GenerationType.IDENTITY
line will use the MySQL autoincrement strategy in the background to insert records into the ttt_company
table. Different databases may require different strategies. For example, an Oracle database table could use a sequence as the basis for primary key generation by defining the following generator annotations:
@GeneratedValue(generator="gen_seq_company") @SequenceGenerator(name="gen_seq_company", sequenceName="seq_id_company")
This is an optional annotation that is used to identify the nullability of the field. The @Basic(optional = false)
line is used to specify that the field is not optional (may not be null). Likewise, the @Basic(optional = true)
line could be used for other fields that may be nullable.
This annotation specifies the column to which the field is mapped. The @Column(name = "id_company")
line will, hence, map the id_company
column in the ttt_company
table to the idCompany
field in the class.
These annotations are part of the javax.validation.constraints
package (the Bean Validation package was introduced in Java EE 6) and define that the field cannot be null as well as the minimum and maximum sizes for the field. The company_name
column in the ttt_company
table was defined as varchar(200) not null
, which is the reason why these annotations were created during the reverse engineering process.
A Company
class may have zero or more Projects
entities. This relationship is defined by the @OneToMany
annotation. In words, we can describe this relationship as One Company can have Many Projects. In JPA, an entity is associated with a collection of other entities by defining this annotation with a mappedBy
property. We have refactored the original mappedBy
value to company
. This will be the name of the field in the Project.java
file after we have refactored the Project
file in the next section.
The Projects.java file
As you may have guessed by now, this file represents the Project
entity and maps to the ttt_project
table. Double-click on the file to open it in the editor and browse the contents. We will once again do a bit of refactoring to clarify the autogenerated fields:
- Rename the autogenerated
idCompany
field tocompany
using the refactoring process. Don't forget to rename the get and set methods. - Rename the autogenerated
taskList
field totasks
. Don't forget the get and set methods again! - Rename the
mappedBy
value fromidProject
toproject
.
The final refactored file should now look like the following code:
package com.gieman.tttracker.domain; import java.io.Serializable; import java.util.List; import javax.persistence.Basic; import javax.persistence.CascadeType; 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.OneToMany; import javax.persistence.Table; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @Entity @Table(name = "ttt_project") @NamedQueries({ @NamedQuery(name = "Project.findAll", query = "SELECT p FROM Project p"), @NamedQuery(name = "Project.findByIdProject", query = "SELECT p FROM Project p WHERE p.idProject = :idProject"), @NamedQuery(name = "Project.findByProjectName", query = "SELECT p FROM Project p WHERE p.projectName = :projectName")}) public class Project implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Basic(optional = false) @Column(name = "id_project") private Integer idProject; @Basic(optional = false) @NotNull @Size(min = 1, max = 200) @Column(name = "project_name") private String projectName; @JoinColumn(name = "id_company", referencedColumnName = "id_company") @ManyToOne(optional = false) private Company company; @OneToMany(cascade = CascadeType.ALL, mappedBy = "project") private List<Task> tasks; public Project() { } public Project(Integer idProject) { this.idProject = idProject; } public Project(Integer idProject, String projectName) { this.idProject = idProject; this.projectName = projectName; } public Integer getIdProject() { return idProject; } public void setIdProject(Integer idProject) { this.idProject = idProject; } public String getProjectName() { return projectName; } public void setProjectName(String projectName) { this.projectName = projectName; } public Company getCompany() { return company; } public void setCompany(Company company) { this.company = company; } public List<Task> getTasks() { return tasks; } public void setTasks(List<Task> tasks) { this.tasks = tasks; } @Override public int hashCode() { int hash = 0; hash += (idProject != null ? idProject.hashCode() : 0); return hash; } @Override public boolean equals(Object object) { if (!(object instanceof Project)) { return false; } Project other = (Project) object; if ((this.idProject == null && other.idProject != null) || (this.idProject != null && !this.idProject.equals(other.idProject))) { return false; } return true; } @Override public String toString() { return "com.gieman.tttracker.domain.Project[ idProject=" + idProject + " ]"; } }
This annotation represents a relationship between entities; it is the reverse of the @OneToMany
annotation. For the Project
entity, we can say that Many Projects have One Company. In other words, a Project
entity belongs to a single Company
class, and (inversely) a Company
class can have any number of Projects
entities. This relationship is defined at the database level (that is, the foreign key relationship in the underlying tables) and is achieved in the @JoinColumn
annotation:
@JoinColumn(name = "id_company", referencedColumnName = "id_company")
The name
property defines the name of the column in the ttt_project
table that is the foreign key to the referencedColumnName
column in the ttt_company
table.
Bidirectional mapping and owning entities
It is essential to grasp the very important concept of how one entity is related to another through the @ManyToOne
and @OneToMany
annotations. The Company
class has a list of mapped Projects
entities defined as follows:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "company") private List<Project> projects;
Whereas, the Project
class has exactly one mapped Company
entity:
@JoinColumn(name="id_company", referencedColumnName="id_company") @ManyToOne(optional=false) private Company company;
This is known as bidirectional mapping, one mapping on each class for each direction. A many-to-one mapping back to the source, as in the Project
entity back to the Company
entity, implies a corresponding one-to-many mapping on the source (Company
) back to the target (Project
). The terms source and target can be defined as follows:
- Source: This is an entity that can exist in a relationship in its own right. The source entity does not require the target entity to exist and the
@OneToMany
collection can be empty. In our example, aCompany
entity can exist without aProject
entity. - Target: This is an entity that cannot exist on its own without a reference to a valid source. The
@ManyToOne
entity defined on the target cannot be null. AProject
entity cannot exist in our design without a validCompany
entity.
The owning entity is an entity that understands the other entity from a database perspective. In simple terms, the owning entity has the @JoinColumn
definition describing the underlying columns that form the relationship. In the Company
-Project
relationship, Project
is the owning entity. Note that an entity can be both a target as well as a source as shown in the following Project.java
file snippet:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "project") private List<Task> tasks;
Here, Project
is the source for the Task
entity relationship and we would expect a reverse @ManyToOne
annotation on the Task
class. This is exactly what we will find.
The Task.java file
This file defines the Task
entity that represents the ttt_task
table. Open the file and perform the following refactoring:
- Delete the autogenerated
taskLogList
field and also delete the associated get and set methods. Why do we do this? There may be many millions of task logs in the system for eachTask
instance and it is not advisable to hold a reference to such a large set ofTaskLog
instances within theTask
object. - Rename the autogenerated
idProject
field toproject
. Don't forget to delete the get and set methods again.
After making the preceding changes, you will see that some of the imports are no longer required and are highlighted by the NetBeans IDE:
The keyboard combination of Ctrl + Shift + I will remove all the unused imports. Another alternative is to click on the icon, shown in the following screenshot, to open the menu and select a Remove option:
It is good practice to have clean code and removing the unused imports is a simple process.
The final refactored file should now look like the following code snippet:
package com.gieman.tttracker.domain; 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; @Entity @Table(name = "ttt_task") @NamedQueries({ @NamedQuery(name = "Task.findAll", query = "SELECT t FROM Task t"), @NamedQuery(name = "Task.findByIdTask", query = "SELECT t FROM Task t WHERE t.idTask = :idTask"), @NamedQuery(name = "Task.findByTaskName", query = "SELECT t FROM Task t WHERE t.taskName = :taskName")}) public class Task implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Basic(optional = false) @Column(name = "id_task") private Integer idTask; @Basic(optional = false) @NotNull @Size(min = 1, max = 200) @Column(name = "task_name") private String taskName; @JoinColumn(name = "id_project", referencedColumnName = "id_project") @ManyToOne(optional = false) private Project project; public Task() { } public Task(Integer idTask) { this.idTask = idTask; } public Task(Integer idTask, String taskName) { this.idTask = idTask; this.taskName = taskName; } public Integer getIdTask() { return idTask; } public void setIdTask(Integer idTask) { this.idTask = idTask; } public String getTaskName() { return taskName; } public void setTaskName(String taskName) { this.taskName = taskName; } public Project getProject() { return project; } public void setProject(Project project) { this.project = project; } @Override public int hashCode() { int hash = 0; hash += (idTask != null ? idTask.hashCode() : 0); return hash; } @Override public boolean equals(Object object) { if (!(object instanceof Task)) { return false; } Task other = (Task) object; if ((this.idTask == null && other.idTask != null) || (this.idTask != null && !this.idTask.equals(other.idTask))) { return false; } return true; } @Override public String toString() { return "com.gieman.tttracker.domain.Task[ idTask=" + idTask + " ]"; } }
Note the @ManyToOne
annotation referencing the Project
class using the @JoinColumn
definition. The Task
object owns this relationship.
The User.java file
The User
entity represents the underlying ttt_user
table. The generated class has a @OneToMany
definition for the relationship to the TaskLog
class:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "username") private List<TaskLog> taskLogList;
Refactoring in this file will once again delete this relationship completely. As noted in the Tasks.java
section, a User
entity may also have many thousands of task logs. By understanding the application's requirements and data structure, it is often far cleaner to remove unnecessary relationships completely.
You will also note that the @Pattern
annotation is commented out by default during the reverse engineering process. The email
field name indicated to NetBeans that this might be an e-mail field and NetBeans added the annotation for use if required. We will uncomment this annotation to enable e-mail pattern checking for the field and add the required import:
import javax.validation.constraints.Pattern;
The refactored User.java
file will now look like the following code snippet:
package com.gieman.tttracker.domain; import java.io.Serializable; import javax.persistence.Basic; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Table; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; @Entity @Table(name = "ttt_user") @NamedQueries({ @NamedQuery(name = "User.findAll", query = "SELECT u FROM User u"), @NamedQuery(name = "User.findByUsername", query = "SELECT u FROM User u WHERE u.username = :username"), @NamedQuery(name = "User.findByFirstName", query = "SELECT u FROM User u WHERE u.firstName = :firstName"), @NamedQuery(name = "User.findByLastName", query = "SELECT u FROM User u WHERE u.lastName = :lastName"), @NamedQuery(name = "User.findByEmail", query = "SELECT u FROM User u WHERE u.email = :email"), @NamedQuery(name = "User.findByPassword", query = "SELECT u FROM User u WHERE u.password = :password"), @NamedQuery(name = "User.findByAdminRole", query = "SELECT u FROM User u WHERE u.adminRole = :adminRole")}) public class User implements Serializable { private static final long serialVersionUID = 1L; @Id @Basic(optional = false) @NotNull @Size(min = 1, max = 10) @Column(name = "username") private String username; @Basic(optional = false) @NotNull @Size(min = 1, max = 100) @Column(name = "first_name") private String firstName; @Basic(optional = false) @NotNull @Size(min = 1, max = 100) @Column(name = "last_name") private String lastName; @Pattern(regexp="[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", message="Invalid email") @Basic(optional = false) @NotNull @Size(min = 1, max = 100) @Column(name = "email") private String email; @Basic(optional = false) @NotNull @Size(min = 1, max = 100) @Column(name = "password") private String password; @Column(name = "admin_role") private Character adminRole; public User() { } public User(String username) { this.username = username; } public User(String username, String firstName, String lastName, String email, String password) { this.username = username; this.firstName = firstName; this.lastName = lastName; this.email = email; this.password = password; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Character getAdminRole() { return adminRole; } public void setAdminRole(Character adminRole) { this.adminRole = adminRole; } @Override public int hashCode() { int hash = 0; hash += (username != null ? username.hashCode() : 0); return hash; } @Override public boolean equals(Object object) { if (!(object instanceof User)) { return false; } User other = (User) object; if ((this.username == null && other.username != null) || (this.username != null && !this.username.equals(other.username))) { return false; } return true; } @Override public String toString() { return "com.gieman.tttracker.domain.User[ username=" + username + " ]"; } }
The TaskLog.java file
The final entity in our application represents the ttt_task_log
table. The refactoring required here is to rename the idTask
field to task
(remember to also rename the get and set methods) and then rename the username
field to user
. The file should now look like the following code snippet:
package com.tttracker.domain; import java.io.Serializable; import java.util.Date; 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.persistence.Temporal; import javax.persistence.TemporalType; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @Entity @Table(name = "ttt_task_log") @NamedQueries({ @NamedQuery(name = "TaskLog.findAll", query = "SELECT t FROM TaskLog t"), @NamedQuery(name = "TaskLog.findByIdTaskLog", query = "SELECT t FROM TaskLog t WHERE t.idTaskLog = :idTaskLog"), @NamedQuery(name = "TaskLog.findByTaskDescription", query = "SELECT t FROM TaskLog t WHERE t.taskDescription = :taskDescription"), @NamedQuery(name = "TaskLog.findByTaskLogDate", query = "SELECT t FROM TaskLog t WHERE t.taskLogDate = :taskLogDate"), @NamedQuery(name = "TaskLog.findByTaskMinutes", query = "SELECT t FROM TaskLog t WHERE t.taskMinutes = :taskMinutes")}) public class TaskLog implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Basic(optional = false) @Column(name = "id_task_log") private Integer idTaskLog; @Basic(optional = false) @NotNull @Size(min = 1, max = 2000) @Column(name = "task_description") private String taskDescription; @Basic(optional = false) @NotNull @Column(name = "task_log_date") @Temporal(TemporalType.DATE) private Date taskLogDate; @Basic(optional = false) @NotNull @Column(name = "task_minutes") private int taskMinutes; @JoinColumn(name = "username", referencedColumnName = "username") @ManyToOne(optional = false) private User user; @JoinColumn(name = "id_task", referencedColumnName = "id_task") @ManyToOne(optional = false) private Task task; public TaskLog() { } public TaskLog(Integer idTaskLog) { this.idTaskLog = idTaskLog; } public TaskLog(Integer idTaskLog, String taskDescription, Date taskLogDate, int taskMinutes) { this.idTaskLog = idTaskLog; this.taskDescription = taskDescription; this.taskLogDate = taskLogDate; this.taskMinutes = taskMinutes; } public Integer getIdTaskLog() { return idTaskLog; } public void setIdTaskLog(Integer idTaskLog) { this.idTaskLog = idTaskLog; } public String getTaskDescription() { return taskDescription; } public void setTaskDescription(String taskDescription) { this.taskDescription = taskDescription; } public Date getTaskLogDate() { return taskLogDate; } public void setTaskLogDate(Date taskLogDate) { this.taskLogDate = taskLogDate; } public int getTaskMinutes() { return taskMinutes; } public void setTaskMinutes(int taskMinutes) { this.taskMinutes = taskMinutes; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public Task getTask() { return task; } public void setTask(Task task) { this.task = task; } @Override public int hashCode() { int hash = 0; hash += (idTaskLog != null ? idTaskLog.hashCode() : 0); return hash; } @Override public boolean equals(Object object) { if (!(object instanceof TaskLog)) { return false; } TaskLog other = (TaskLog) object; if ((this.idTaskLog == null && other.idTaskLog != null) || (this.idTaskLog != null && !this.idTaskLog.equals(other.idTaskLog))) { return false; } return true; } @Override public String toString() { return "com.tttracker.domain.TaskLog[ idTaskLog=" + idTaskLog + " ]"; } }