Pixel Core Tutorial – Data Model

Pixel Core Tutorial – Data Model

In general Pixel Core Framework adhere to following rule – all data objects are simple POJOs with no business logic.

There are number of interfaces and annotations that add syntactic sugar, make code more readable with less repeating blocks.

public class GameDef
{
private Integer id;
private Code code;
private Boolean enabled;
private Client client;
private List<Nls> nls;
private Date created;
private Date updated;
private String updatedBy;
}

Lets go over all properties of this class

id

This is a unique identifier that will never change over lifetime of the object. It is not necessary must be integer. It could be of any type.

code

It is human readable unique identifier of the object. It is introduced for interaction with other systems. Lets say we have 3rd party reporting system that needs to query our system. For developers it will be much easier and less error prone to request something like https://pixelnation.com/games/get-by-code?code=NUMBERS-GAME. Please keep in mind that reporting is a 3rd party company and does not have luxury of requesting custom functionality on our side.

enabled

We want to control our object state. It can be enabled or disabled. Based on that flag, UI will function or not.

client

Pixel Core provides Client class out of the box. By adding this field we letting system know that instances of this object will be handled according to specific rules. Lets assume that  one instance I1 belongs to client C1 and another I2 to client C2. And we have two users one of which U1 belongs to client C1 and another U2 to C2. Then user U1 will never be able to access or even know that there is instance I2 and vice versa U2 will never know about I1.

nls

NLS properties are optional. They are required if we want to present data in different languages. Lets say we want to create a gaming platform and support multi-lingual games. Obviously the administrative interface must allow for data to be localized. Nls class contains language, name and description. Lets assume that we created a game “Game of Thrones” and our UI is language agnostic and downloads the game code or properties over Internet. In this case, if UI on the client is configured to French we want to provide “Le Trône de Fer” to the client. NLS object will provide us with this functionality.

created and updated

We want to trace when object been created and when it was updated last. We are not interested in keeping all history of changes. For that functionality you can refer to Hibernate Envers. So two fields is enough.

updatedBy

We also want to know who was creating/updating the object last. This field will contain user name.

Annotations

Now, as we defined our data model, we can prepare it for framework. We will add number of annotations to the class and explain why we’ve done it.

@Notification

This custom Pixel Core annotation @Notification instructs framework to send notifications to subscribed clients and components when object is created, updated or deleted. In general only ID of the object will be sent and it is up to the subscribed component to act accordingly.

@DataTransferObject(converter = H3BeanConverter.class, javascript = “GameDef”)

This is DWR annotation telling framework to export this class to JavaScript clients.

@Cacheable


@Cache(region = “GameDef”, usage = CacheConcurrencyStrategy.READ_WRITE)

Both annotations are instructing Hibernate to cache and where to cache instances of this class. Cache configuration defining how much memory to allocate for cache, how many instances to keep and for how long is defined in separate configuration files and depends on cache implementation used in particular project.

@Entity


@Table(name = “game_def”)

These are pure Hibernate annotations linking this class with database layer. @Entity is telling the engine that this entity is persistent. In general @Table annotation is not necessary, but we want to have control over how class names will be mapped to table names in database. Without this annotation database would create gamedef table.

@SequenceGenerator(name = “game_def_identifier”, sequenceName = “seq_game_def”, allocationSize = 1)

Using sequence generator annotation we instruct the system to create sequence in the database with name seq_game_def. This annotation goes in tandem with field based annotation on id property.

@Id


@Column(name = “game_def_id”, unique = true, nullable = false)


@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = “game_def_identifier”)

Those annotations are placed on id property to mark it as primary key for database access. @GeneratedValue is defined as sequence and references sequence generator annotated at the class level.

@RemoteProperty

This will tell DWR to include this property into GameDef class visible in JavaScript client.

 

@Notification
@DataTransferObject(converter = H3BeanConverter.class, javascript = "GameDef")
@Entity
@Cacheable
@Cache(region = "GameDef", usage = CacheConcurrencyStrategy.READ_WRITE)
@Table(name = "game_def")
@SequenceGenerator(name = "game_def_identifier", sequenceName = "seq_game_def", allocationSize = 1)
public class Game
{
@Id
@Column(name = "game_def_id", unique = true, nullable = false)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "game_def_identifier")
private Integer id;
@Column(name = "code", unique = true, nullable = false)
private Code code;
@Column(name = "enabled")
private Boolean enabled;
@ManyToOne
@JoinColumn(name = "client_id")
private Client client;
@ElementCollection
@CollectionTable(name = "game_def_nls", joinColumns = @JoinColumn(name = "game_def_id"), uniqueConstraints = @UniqueConstraint(columnNames = { "game_def_id", "lang_code3" }))
@Cache(region = "GameDefNLS", usage = CacheConcurrencyStrategy.READ_WRITE)
private List<Nls> nls;
@Column(name = "created")
private Date created;
@Column(name = "updated")
private Date updated;
@Column(name = "updated_by")
private String updatedBy;

@RemoteProperty
public Integer getId() { return id; }
public void setId(Integer pId) { id = pId; }
... more POJO getters and setters
}

As you can see, there are a lot of properties that are repeated from class to class. Using AspectJ we can reduce amount of code and make it more readable.

@ClientAware annotation will automatically inject field client and associated getter and setter. So this field could be removed from class declaration.

Interface Traceable will take care of created, updated and updatedBy fields. AspectJ will inject those fields automatically

Interface Enableable will substitute enabled field.

Interface Codeable will take care of code field and methods.

Adding Optimistic interface will make sure that save of older object state will not overwrite newer changes. See locking documentation at RedHat

Localizable interface is used within persistence layer. Unfortunately, it is not easy to annotate class with proper generated names for database mappings. At the moment nls field and access methods still have to be present in the class definition. Work is being done to rectify this shortcoming in the next major release.

After applying interfaces and annotations class definition

package com.pixelnation.gaming.engine.domain;

import java.util.List;

import javax.persistence.Cacheable;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;

import org.directwebremoting.annotations.DataTransferObject;
import org.directwebremoting.annotations.RemoteProperty;
import org.directwebremoting.hibernate.H3BeanConverter;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import com.pixelnation.common.annotations.ClientAware;
import com.pixelnation.common.annotations.Notification;
import com.pixelnation.common.domain.Nls;
import com.pixelnation.common.domain.iface.Codeable;
import com.pixelnation.common.domain.iface.Enableable;
import com.pixelnation.common.domain.iface.Identifiable;
import com.pixelnation.common.domain.iface.Localizable;
import com.pixelnation.common.domain.iface.Optimistic;
import com.pixelnation.common.domain.iface.Traceable;

@Notification
@DataTransferObject(converter = H3BeanConverter.class, javascript = "GameDef")
@Entity
@Cacheable
@Cache(region = "GameDef", usage = CacheConcurrencyStrategy.READ_WRITE)
@Table(name = "game_def")
@SequenceGenerator(name = "game_def_identifier", sequenceName = "seq_game_def", allocationSize = 1)
@ClientAware
public class GameDef implements Identifiable, Codeable, Enableable, Localizable, Traceable, Optimistic
{
private static final long serialVersionUID = -5115148188260673856L;
@Id
@Column(name = "game_def_id", unique = true, nullable = false)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "game_def_identifier")
private Integer id;
@ElementCollection
@CollectionTable(name = "game_def_nls", joinColumns = @JoinColumn(name = "game_def_id"), uniqueConstraints = @UniqueConstraint(columnNames = { "game_def_id", "lang_code3" }))
@Cache(region = "GameDefNLS", usage = CacheConcurrencyStrategy.READ_WRITE)
private List nls;

@Override
@RemoteProperty
public Integer getIdentifier()
{
return id;
}

@RemoteProperty
public Integer getId()
{
return id;
}

public void setId(Integer pId)
{
id = pId;
}

@Override
@RemoteProperty
public List getNls()
{
return nls;
}

@Override
public void setNls(List pNls)
{
nls = pNls;
}

@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((getCode() == null) ? 0 : getCode().hashCode());
return result;
}

@Override
public boolean equals(Object obj)
{
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
GameDef other = (GameDef) obj;
if (getCode() == null)
{
if (other.getCode() != null) return false;
}
else if (!getCode().equals(other.getCode())) return false;
return true;
}

@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
builder.append("GameDef [");
if (id != null)
{
builder.append("id=");
builder.append(id);
}
if (getClient() != null)
{
builder.append(", client=");
builder.append(getClient().getCode());
}
if (getEnabled() != null)
{
builder.append(", enabled=");
builder.append(getEnabled());
}
if (getOptimistic() != null)
{
builder.append(", optimistic=");
builder.append(getOptimistic());
}
if (getCreated() != null)
{
builder.append(", created=");
builder.append(getCreated());
}
if (getUpdated() != null)
{
builder.append(", updated=");
builder.append(getUpdated());
}
if (getUpdatedBy() != null)
{
builder.append(", updatedBy=");
builder.append(getUpdatedBy());
}
builder.append("]");
return builder.toString();
}
}

Note, how getClient(), getCode(), getCreated(), getUpdated(), getUpdatedBy() and getOptimistic() methods appeared without declaration.

We will also create two more classes GameMetrics and GameRun.

GameRun class will hold results of each individual game played. GameMetrics will hold definitions of all metrics stored in GameRun.metrics field.

GameRun

package com.pixelnation.gaming.engine.domain;

import java.util.Map;

import javax.persistence.Cacheable;
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.SequenceGenerator;
import javax.persistence.Table;

import org.directwebremoting.annotations.DataTransferObject;
import org.directwebremoting.annotations.RemoteProperty;
import org.directwebremoting.hibernate.H3BeanConverter;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Parameter;
import org.hibernate.annotations.Type;

import com.pixelnation.common.annotations.Notification;
import com.pixelnation.common.domain.iface.Identifiable;
import com.pixelnation.common.domain.iface.Traceable;
import com.pixelnation.hibernate.usertype.JSONBPostgresUserType;

@Notification
@DataTransferObject(converter = H3BeanConverter.class, javascript = "GameRun")
@Entity
@Cacheable
@Cache(region = "GameRun", usage = CacheConcurrencyStrategy.READ_WRITE)
@Table(name = "game_run")
@SequenceGenerator(name = "game_run_identifier", sequenceName = "seq_game_run", allocationSize = 1)
public class GameRun implements Identifiable, Traceable
{
private static final long serialVersionUID = 7880594622254904504L;
@Id
@Column(name = "game_run_id", unique = true, nullable = false)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "game_def_identifier")
private Long id;
@ManyToOne
@JoinColumn(name = "game_def_id", nullable = false)
private GameDef gameDef;
@Column(name = "metrics", columnDefinition = "jsonb")
@Type(type = "jsonb", parameters = @Parameter(name = JSONBPostgresUserType.CLASS, value = "Map<java.lang.String, java.lang.Object>"))
private Map<String, Object> metrics;

@Override
@RemoteProperty
public Long getIdentifier()
{
return id;
}

@RemoteProperty
public Long getId()
{
return id;
}

public void setId(Long pId)
{
id = pId;
}

@RemoteProperty
public GameDef getGameDef()
{
return gameDef;
}

public void setGameDef(GameDef pGameDef)
{
gameDef = pGameDef;
}

@RemoteProperty
public Map<String, Object> getMetrics()
{
return metrics;
}

public void setMetrics(Map<String, Object> pMetrics)
{
metrics = pMetrics;
}

@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}

@Override
public boolean equals(Object obj)
{
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
GameRun other = (GameRun) obj;
if (id == null)
{
if (other.id != null) return false;
}
else if (!id.equals(other.id)) return false;
return true;
}

@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
builder.append("GameRun [");
if (id != null)
{
builder.append("id=");
builder.append(id);
builder.append(", ");
}
if (gameDef != null)
{
builder.append("gameDef=");
builder.append(gameDef.getCode());
builder.append(", ");
}
if (metrics != null)
{
builder.append("metrics=");
builder.append(metrics);
}
builder.append("]");
return builder.toString();
}
}

 

GameMetrics

 

package com.pixelnation.gaming.engine.domain;

import java.util.List;

import javax.persistence.Cacheable;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
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.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;

import org.directwebremoting.annotations.DataTransferObject;
import org.directwebremoting.annotations.RemoteProperty;
import org.directwebremoting.hibernate.H3BeanConverter;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import com.pixelnation.common.domain.Nls;
import com.pixelnation.common.domain.iface.Identifiable;
import com.pixelnation.common.domain.iface.Localizable;

@DataTransferObject(converter = H3BeanConverter.class, javascript = "GameMetrics")
@Entity
@Cacheable
@Cache(region = "GameMetrics", usage = CacheConcurrencyStrategy.READ_WRITE)
@Table(name = "game_metric", uniqueConstraints = { @UniqueConstraint(columnNames = { "code", "resource_type_id" }) })
@SequenceGenerator(name = "game_metric_identifier", sequenceName = "seq_game_metric", allocationSize = 1)
public class GameMetrics implements Identifiable, Localizable
{
private static final long serialVersionUID = 7345889426663477326L;

@Id
@Column(name = "game_metric_id", unique = true, nullable = false)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "game_metric_identifier")
private Integer id;

// This class is not Codeable because of the constraint!!!
@Column(name = "code", nullable = false, length = 64)
private String code;

@Column(name = "data_type", nullable = false)
private String dataType;

@Column(name = "sort_order", nullable = false)
private Integer sortOrder = 0;

@ManyToOne
@JoinColumn(name = "resource_type_id", insertable = false, updatable = false)
private GameDef gameDef;

@ElementCollection
@CollectionTable(name = "game_metric_nls", joinColumns = @JoinColumn(name = "game_metric_id"), uniqueConstraints = @UniqueConstraint(columnNames = { "game_metric_id", "lang_code3" }))
@Cache(region = "GameMetricsNLS", usage = CacheConcurrencyStrategy.READ_WRITE)
private List nls;

@RemoteProperty
@Override
public Integer getIdentifier()
{
return id;
}

/**
* @return the id
*/
@RemoteProperty
public Integer getId()
{
return id;
}

/**
* @param pId the id to set
*/
public void setId(Integer pId)
{
id = pId;
}

@RemoteProperty
public String getCode()
{
return code;
}

public void setCode(String pCode)
{
code = pCode;
}

@RemoteProperty
public String getDataType()
{
return dataType;
}

public void setDataType(String pDataType)
{
dataType = pDataType;
}

/**
* @return the gameDef
*/
@RemoteProperty
public GameDef getGameDef()
{
return gameDef;
}

/**
* @param pGameDef the gameDef to set
*/
public void setGameDef(GameDef pGameDef)
{
gameDef = pGameDef;
}

/**
* @return the nls
*/
@Override
@RemoteProperty
public List getNls()
{
return nls;
}

/**
* @param pNls the nls to set
*/
@Override
public void setNls(List pNls)
{
nls = pNls;
}

@RemoteProperty
public Integer getSortOrder()
{
return sortOrder;
}

public void setSortOrder(Integer pSortOrder)
{
sortOrder = pSortOrder;
}

@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((getCode() == null) ? 0 : getCode().hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((gameDef == null) ? 0 : gameDef.hashCode());
return result;
}

@Override
public boolean equals(Object obj)
{
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
GameMetrics other = (GameMetrics) obj;
if (getCode() == null)
{
if (other.getCode() != null) return false;
}
else if (!getCode().equals(other.getCode())) return false;
if (id == null)
{
if (other.id != null) return false;
}
else if (!id.equals(other.id)) return false;
if (gameDef == null)
{
if (other.gameDef != null) return false;
}
else if (!gameDef.equals(other.gameDef)) return false;
return true;
}

@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
builder.append("GameMetrics [id=");
builder.append(id);
builder.append(", code=");
builder.append(getCode());
builder.append(", dataType=");
builder.append(dataType);
builder.append(", gameDef=");
builder.append(gameDef == null ? "null" : gameDef.getCode());
builder.append(", nls=");
builder.append(nls);
builder.append("]");
return builder.toString();
}
}

Next tutorial will describe how to create basic persistence manager.