Co zrobić, aby budując system oparty o tradycyjną, relacyjną bazę danych oraz Toplink JPA wykorzystać w pełni siłę języka Java i JPQL? Tak by móc konstruować zapytania typu:
IpAddress ipAddr = new IpAddress("10.0.0.2");
TDhcp dhcp = em.createQuery("SELECT d from TDhcp d WHERE d.ipAddress=:ip")
.setParameter("ip", ipAddr)
.getSingleResult();
MacAddress mac = new MacAddress("00:21:70:83:ff:ff");
TDhcp dhcp2 = em.createQuery("SELECT d from TDhcp d WHERE d.macAddress=:mac")
.setParameter("mac", mac)
.getSingleResult();
podczas gdy w bazie danych ip zapisane jest jako zwykły, brzydki integer, a macaddress jako string ?
Nic prostszego! Możemy przygotować mechanizm mapowania.
Zapraszam więc do lektury (i komentowania) artykułu w którym postaram się przedstawić przykład wykorzystania mapowania pomiędzy bazodanowym typem integer a naszym własnym typ IpAddress.
Tabela t_dhcp:
CREATE TABLE `t_dhcp` ( `ip_address` int(10) unsigned NOT NULL, `mac_address` char(12) NOT NULL, `lease_due` datetime default NULL, PRIMARY KEY (`ip_address`)
Entity classTDhcp dla tabeli t_dhcp. W podświetlonych liniach widać jak proste jest użycie raz przygotowanego konwertera (właściwie dwóch konwerterów – dla IpAddress i MacAddress)
package eu.swierczyna.ipmanager.entity;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import eu.swierczyna.ipmanager.IpAddress;
import eu.swierczyna.ipmanager.MacAddress;
import eu.swierczyna.ipmanager.toplink.Converter;
import eu.swierczyna.ipmanager.toplink.ConverterType;
@Entity
@Table(name="t_dhcp")
public class TDhcp implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name="ip_address", nullable=false)
@Converter(type=ConverterType.IP_ADDRESS_CONVERTER)
private IpAddress ipAddress;
@Temporal( TemporalType.TIMESTAMP)
@Column(name="lease_due")
private Date leaseDue;
@Column(name="mac_address")
@Converter(type=ConverterType.MAC_ADDRESS_CONVERTER)
private MacAddress macAddress;
// getters and setters
public IpAddress getIpAddress() {
return ipAddress;
}
public void setIpAddress(IpAddress ipAddress) {
this.ipAddress = ipAddress;
}
public Date getLeaseDue() {
return leaseDue;
}
public void setLeaseDue(Date leaseDue) {
this.leaseDue = leaseDue;
}
public MacAddress getMacAddress() {
return macAddress;
}
public void setMacAddress(MacAddress macAddress) {
this.macAddress = macAddress;
}
}
Plik persistence.xml
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
<persistence-unit name="IpManager" transaction-type="JTA">
<jta-data-source>jdbc/ipmanager</jta-data-source>
<class>eu.swierczyna.prov.ipmanager.entity.TDhcp</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<!-- wskazanie dla Toplinka by przeglądnął klasę pod kontem występowania naszych anotacji -->
<!-- musimy dodać taki wpis do każdej z entity class -->
<property name="toplink.descriptor.customizer.TDhcp" value="eu.swierczyna.prov.ipmanager.toplink.DescriptorCustomizer"/>
</properties>
</persistence-unit>
</persistence>
Poniżej klasa IpAddress wraz z metodami których będzie używał konwerter. Metody te oczywiście nie muszą być prywatne.(Implementacja mocno uproszczona na potrzeby artykułu)
package eu.swierczyna.ipmanager;
import java.io.Serializable;
public class IpAddress implements Serializable{
private long value;
public static IpAddress toIpAddress(Long value) {
if (value == null || value.longValue()==0 ){
return null;
} else {
return new IpAddress(value);
}
}
public static long toNumber(IpAddress ip) {
if (ip==null){
return 0;
} else {
return ip.toNumber();
}
}
public IpAddress(){
value=0;
}
public IpAddress(String value){
// TODO: implementacja pominięta dla uproszczenia przykładu
}
/**
* Get address type
* @return private or public
*/
public String getType(){
// TODO: implementacja pominięta dla uproszczenia przykładu
return "private";
}
/**
* @param val ip adres w postaci liczby long. Choć tak naprawdę to liczba 32 bit, czyli integer ale w java mamy tylko signed int (od -2^31 do 2^31-1)
*/
public IpAddress(long val){
if (val<0 || val>(Math.pow(2,32))){
throw new IllegalArgumentException("Niepoprawna wartość adresu ip: "+val);
}
this.value = val;
}
public long toNumber(){
return value;
}
}
Implementacja klasy zapewniającej obukierunkowe mapowanie bazodanowego typu integer na nasz własny, javowy typ IpAddress. Ilementujemy dwie metody w interfejsie Converter dostarczanym przez toplink. Każda metoda dopowiada za mapowanie w jednym kierunku.
package eu.swierczyna.ipmanager.toplink;
import oracle.toplink.essentials.mappings.DatabaseMapping;
import oracle.toplink.essentials.mappings.converters.Converter;
import oracle.toplink.essentials.sessions.Session;
import eu.swierczyna.ipmanager.IpAddress;
public class IpAddressConverter implements Converter {
/**
* Konwersja z typu bazodanowego na typ java
*/
@Override
public Object convertDataValueToObjectValue(Object data, Session arg1) {
if (data==null){
return null;
}
if (data instanceof Long){
IpAddress result = IpAddress.toIpAddress((Long)data);
return result;
} else {
throw new IllegalArgumentException("We need "+Long.class.getCanonicalName()+" as input");
}
}
/**
* Konwersja z typu java na typ bazodanowy
*/
@Override
public Object convertObjectValueToDataValue(Object object, Session arg1) {
if (object==null){
return null;
}
if (object instanceof IpAddress){
Long result = IpAddress.toNumber((IpAddress)object);
return result;
} else {
throw new IllegalArgumentException("We need "+IpAddress.class.getCanonicalName()+" as input");
}
}
@Override
public boolean isMutable() {
return false;
}
}
Aby spiąć klasę konwertującą IpAddressConverter z naszym typem IpAddress, tak by możliwe było używanie anotacji: @Converter(type=ConverterType.IP_ADDRESS_CONVERTER) musimy zaimplementować jeszcze trzy klasy. O ile pierwsza z nich: ConverterType wymaga dostosowana do naszego własnego projektu, o tyle pozostałe (Converter, DescriptorCustomizer) są w zasadzie uniwersalne i mogą zostać przeklejone do innych projektów.
package eu.swierczyna.ipmanager.toplink;
public enum ConverterType {
MAC_ADDRESS_CONVERTER(MacAddressConverter.class),
IP_ADDRESS_CONVERTER(IpAddressConverter.class);
private Class clazz;
ConverterType(Class clazz) {
this.clazz = clazz;
}
public Class getConverter() {
return clazz;
}
}
package eu.swierczyna.ipmanager.toplink;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target( { FIELD, METHOD })
public @interface Converter {
ConverterType type();
}</pre>
<pre>
package eu.swierczyna.ipmanager.toplink;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import oracle.toplink.essentials.descriptors.ClassDescriptor;
import oracle.toplink.essentials.mappings.DatabaseMapping;
import oracle.toplink.essentials.mappings.DirectToFieldMapping;
public class DescriptorCustomizer implements oracle.toplink.essentials.tools.sessionconfiguration.DescriptorCustomizer {
public DescriptorCustomizer() {
}
public void customize(ClassDescriptor desc) throws Exception {
List<DatabaseMapping> mappings = desc.getMappings();
for (DatabaseMapping mapping : mappings) {
if (mapping instanceof DirectToFieldMapping) {
DirectToFieldMapping directMapping = (DirectToFieldMapping) mapping;
String attributeName = mapping.getAttributeName();
Class mappedClazz = mapping.getDescriptor().getJavaClass();
checkFields(directMapping, attributeName, mappedClazz);
checkMethods(directMapping, attributeName, mappedClazz);
}
}
}
private void checkMethods(DirectToFieldMapping mapping, String attributeName, Class clazz) throws InstantiationException, IllegalAccessException {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.getName().equals(mapping.getGetMethodName())
|| method.getName().equals(mapping.getSetMethodName())) {
Annotation[] annons = method.getAnnotations();
checkAnnonations(mapping, annons);
}
}
}
private void checkFields(DirectToFieldMapping directMapping, String attributeName, Class clazz) throws InstantiationException, IllegalAccessException {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.getName().equals(attributeName)) {
Annotation[] annotations = field.getAnnotations();
checkAnnonations(directMapping, annotations);
}
}
}
private void checkAnnonations(DirectToFieldMapping directMapping, Annotation[] annotations) throws InstantiationException, IllegalAccessException {
for (Annotation annon : annotations) {
if (annon.annotationType().equals(Converter.class)) {
Converter convAnnon = (Converter) annon;
ConverterType type = convAnnon.type();
Class converterClass = type.getConverter();
directMapping.setConverter((oracle.toplink.essentials.mappings.converters.Converter) converterClass.newInstance());
}
}
}
}
Poniżej zestaw klas koniecznych do wykonania mapowania bazodanowego varchar’a na nasz własny typ Java – MacAddress. Zestaw klas oraz ich implementacja jest analogiczna dla mapowania int na IpAddress
Klasa MacAddress z rozbudowaną funkcjonalnością konwersji z/do stringów w różnej postaci.
package eu.swierczyna.ipmanager;
import java.io.Serializable;
import java.util.Arrays;
/**
* Reprezentacja adresu MAC
*
* @author sebastian@swierczyna.eu
* @since 2010-01-25
*/
public class MacAddress implements Serializable {
private static final long serialVersionUID = -3207326186299917122L;
private static final int LEN = 6;
private int [] value = new int[LEN];
public MacAddress(){
for (int i=0;i<LEN;i++){
value[i]=0;
}
}
public static MacAddress toMac(String mac) {
if (mac==null) {
return null;
} else {
return new MacAddress(mac);
}
}
public static String toStringSimple(MacAddress mac) {
if (mac==null) {
return null;
} else {
return mac.toStringSimple();
}
}
public MacAddress(String val){
if (val==null){
throw new IllegalArgumentException("Argument can't be null");
} else {
String mac = val.replaceAll(":", "").replaceAll("-", "").replaceAll("\\.", "").replaceAll("\\s", ""). toUpperCase();
if (mac.length()!=LEN*2){
throw new IllegalArgumentException("Incorrect lenght value: "+val);
}
for (int i=0;i<LEN;i++){
String x = mac.substring(i*2, (i+1)*2);
try {
value[i]=Integer.parseInt(x,16);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Argument: "+val+" contains incorrect value: "+x);
}
}
}
}
/**
* return mac as xx:xx:xx:xx:xx:xx
*/
public String toString(){
StringBuffer buffer = new StringBuffer();
for (int i=0;i<LEN;i++){
String s =Integer.toHexString(value[i]).toUpperCase();
if (s.length()==1){
buffer.append('0');
}
buffer.append(s);
if (i<LEN-1){
buffer.append(":");
}
}
return new String(buffer);
}
/**
* return mac as xxxxxxxxxxxx
*/
public String toStringSimple(){
return toString().replaceAll(":", "");
}
/**
* return mac as xxxx.xxxx.xxxx
*/
public String toStringCisco(){
String c = toStringSimple().toLowerCase();
return c.substring(0, 4)+'.'+c.substring(4, 8)+'.' +c.substring(8, 12);
}
public static void main(String args[]){
MacAddress m = new MacAddress("00:20.40-c5 62 fe");
System.out.println(m);
System.out.println(m.toStringSimple());
System.out.println(m.toStringCisco());
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(value);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MacAddress other = (MacAddress) obj;
if (!Arrays.equals(value, other.value))
return false;
return true;
}
}
Oraz klasa konwertera varchar <-> MacAddress
package eu.swierczyna.ipmanager.toplink;
import oracle.toplink.essentials.mappings.DatabaseMapping;
import oracle.toplink.essentials.mappings.converters.Converter;
import oracle.toplink.essentials.sessions.Session;
import org.apache.log4j.Logger;
import eu.swierczyna.ipmanager.MacAddress;
/**
* Converter for Toplink JPA
* Automaticly convert between java MacAddress objects to database String objects
*
* @author sebastian@swierczyna.eu
* @since 2010-02-26
*/
public class MacAddressConverter implements Converter {
private static final long serialVersionUID = -7142128600764340149L;
private static Logger log = Logger.getLogger(MacAddressConverter.class);
@Override
public Object convertDataValueToObjectValue(Object data, Session arg1) {
if (data==null){
return null;
}
log.trace("db->java. Input class: [" + data.getClass().getCanonicalName()+ "], value: ["+ data+"]");
if (data instanceof String){
MacAddress result = MacAddress.toMac((String)data);
log.trace("returning: [" + result.getClass().getCanonicalName()+ "], value: ["+ result+"]");
return result;
} else {
throw new IllegalArgumentException("We need "+String.class.getCanonicalName()+" as input");
}
}
@Override
public Object convertObjectValueToDataValue(Object object, Session arg1) {
if (object==null){
return null;
}
log.trace("java->db. Input class: [" + object.getClass().getCanonicalName()+ "], value: ["+ object+"]");
if (object instanceof MacAddress){
String result = MacAddress.toStringSimple((MacAddress)object);
log.trace("returning: [" + result.getClass().getCanonicalName()+ "], value: ["+ result+"]");
return result;
} else {
throw new IllegalArgumentException("We need "+MacAddress.class.getCanonicalName()+" as input");
}
}
@Override
public void initialize(DatabaseMapping mapping, Session arg1) {
log.trace("Appling for table: "+mapping.getDescriptor().getDefaultTable().getName());
}
@Override
public boolean isMutable() {
return false;
}
}
COMMENTS
No Comments