Architecture solutions: polymorphism basics and advanced inheritance with Hibernate
This is the first post in architecture solutions series. In this series I'm going to talk about different architecture solutions used in my projects that I think are good enough to describe and re-use.
Problem
We need to develop a simple forum engine. Registered users after authentication can create topics and write posts. Authors can edit their posts within 15 minutes after publication.
Anonymous users can only write posts. They can't edit their post afterwards. Also while writing a post they must fill in 2 additional fields: nickname and CAPTCHA. Next time the nickname field is shown, it’s filled with the data entered when making a previous post. CAPTCHA is entered only once and later the field is not showed. Registration form behaves exactly the same.
Thoughts
Architecture for this system can be developed in different ways, but some of them look not very smart. For example, we can create 2 entities: Account (persistent entity for registered users) and Post. This way Post has two fields: Account author (reference to the Account object) and String anonymousAuthorNickname. When a post is made by an anonymous user, we store his nickname in anonymousAuthorNickname field. Otherwise, when post is made by a registered user, we can leave the anonymousAuthorNickname field empty and store a link to the author’s Account entity in the Account author field.
For the implementation of this architecture we can see different problems:
- During post creation or display we have to make a check whether this post is created by anonymous or registered user, because it determines the way author’s data is stored in the object, the logic of display and authorization.
- If we use Hibernate validators we have to duplicate the validator on Account's nickname field and Post's anonymousAuthorNickname field.
- Where should we store information about CAPTCHA and anonymous login (if entered)? Should we store them in the session?
Looks like too much trouble to use in a real-world situation. :)
Let's try another way which looks better: we can persist all users both registered and anonymous. But we have a new problem: the real lifetime of anonymous users’ accounts is limited to a session. Persisting hundreds of anonymous accounts only because it looks more convenient... hmm... not smart on my opinion. By the way we also should use inheritance to separate registered users' data (password, email, etc) and anonymous users’ data (is CAPTCHA entered, etc).
Is there a better way? The truth is out there ;)
I see the third way which doesn't have the disadvantages of the previous two.
Solution
I see two categories of users:
- Registered. Information about users from this category should be persisted, because it can be restored between sessions.
- Anonymous. Information about these users lives only during current session and the best way to store it is to put it in the session.
At the same time I see 2 categories of posts:
- Posts by registered authors. These posts have links to their authors.
- Posts by anonymous authors. These posts don't have links to authors, but have anonymousAuthorNickname field.
As we can see there are two hierarchies of inheritance. Use of inheritance gives us an ability to use polymorphism ... and the easiest solution to described problems.
Looks like magic! Ok. What do we do exactly?
We have an Account instance in the session. This instance could be the instance of subclass RegisteredAccount or subclass AnonymousAccount.
Suddenly user decides to make a post. It means that we invoke method Post loggedInAccount.getNewPost() and receive an instance of Post which could be the instance of either RegisteredAuthorPost or AnonymousAuthorPost. This Post instance is already initialized with actual data depending on current Account instance.
What should we do when a user tries to see posts in a topic? We should simply make a query to the database which returns all posts for requested topic.
How do we understand if user can edit the post? It is as simple as the post creation! We should invoke Boolean post.canBeEdited(loggedInAccount). The post makes a decision by itself if current account is allowed to edit it.
Now let's imagine what it would have looked like without polymorphism. Nightmare!
Now, how it looks in practice.
The first hierarchy.
@MappedSuperclass
public abstract class Account {
private String nickname;
@NotEmpty(message = "#{messages['account.error.nickname.empty']}")
@Length(min = 2, max = 40, message = "#{messages['account.error.nickname.length']}")
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
@Transient
public abstract Post getNewPost();
}
public class AnonymousAccount extends Account {
private Boolean captchaEntered;
public Post getNewPost() {
AnonymousAuthorPost newPost = new AnonymousAuthorPost();
newPost.setAuthorNickname(getNickname());
return newPost;
}
public boolean isCaptchaEntered() {
return captchaEntered;
}
public void setCaptchaEntered (Boolean entered) {
this.captchaEntered = entered;
}
}
@Entity
public class RegisteredAccount extends Account {
private Long id;
private Date creationDate = new Date();
private String password;
private String email;
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
@Email(message = "#{messages['post.error.mail.format']}")
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@NotEmpty(message = "#{messages['account.error.password.empty']}")
@Length(min = 6, max = 40, message = "#{messages['account.error.password.length']}")
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Transient
public Post getNewPost() {
RegisteredAuthorPost newPost = new RegisteredAuthorPost();
newPost.setAuthor(this);
return newPost;
}
}
As you can see only RegisteredAccount has @Entity annotation and only this entity is persisted. AnonymousAccount lives while session lives.
Also both types of account know what kind of Post they should create.
The second hierarchy.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
name="author_type",
discriminatorType=DiscriminatorType.STRING
)
@DiscriminatorValue("unknown")
public abstract class Post {
private Long id;
private String body;
private Topic topic;
private Date creationDate = new Date();
private Date deletionDate;
private Status status = Status.ACTIVE;
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@NotNull
@Length(min=1, max=4000, message = "#{messages['post.error.body.length']}")
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
@ManyToOne
public Topic getTopic() {
return topic;
}
public void setTopic(Topic topic) {
this.topic = topic;
}
@NotNull
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
@NotNull
@Enumerated(EnumType.STRING)
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public abstract String getAuthorNickname();
public abstract void setAuthorNickname(String authorNickname);
@Transient
public abstract boolean canBeEdited(Account usingAccount);
}
@Entity
@DiscriminatorValue("anonymous")
@AttributeOverride(name = "authorNickname", column = @Column(name="anonymousAuthorNickname"))
public class AnonymousAuthorPost extends Post {
String authorNickname;
@Transient
public String getDisplayingStyleClass() {
return Messages.instance().get("anonymous-author-post-style-class");
}
@Length(max = 50, message = "#{messages['account.error.nickname.length']}")
public String getAuthorNickname() {
return authorNickname;
}
public void setAuthorNickname(String authorNickname) {
this.authorNickname = authorNickname;
}
@Transient
public boolean canBeEdited(Account usedAccount) {
return false;
}
}
@Entity
@DiscriminatorValue("registered")
public class RegisteredAuthorPost extends Post {
RegisteredAccount author;
@NotNull
@ManyToOne
public RegisteredAccount getAuthor() {
return author;
}
public void setAuthor(RegisteredAccount author) {
this.author = author;
}
@Transient
public String getAuthorNickname() {
return getAuthor().getNickname();
}
@Transient
public void setAuthorNickname(String authorNickname) {
//nothing to do
}
@Transient
private boolean isEditTimeElapsed() {
…
//check time here
}
@Transient
public boolean canBeEdited(Account usedAccount) {
if (usedAccount instanceof AnonymousAccount) return false;
if (getAuthor().equals(usedAccount) && !isEditTimeElapsed()) return true;
return false;
}
}
Posts can be created both by registered and anonymous users, so we have two subclasses. Polymorphism gives us an ability to get nickname either from String field (for anonymous author) or from linked account object.
Conclusion
Architecture of this example is rather simple, but it shows how we can successfully use such rare used features of Hibernate as inheritance. Also this example demonstrates that mapping can be successfully used even for only one subclass of some unmapped abstract class.
Also you can see how to use persistence with inherited entities. Here we use one table mode with discriminator field and non-trivial usage of polymorphism: for one inherited entity methods are real getters-setters, but for the other one those methods are defined as @Transient and are used only to access linked object’s data.

0 comments:
Post a Comment