me and my team are working on an upgrade of our company's system which as getting kind of forgotten and was running old versions of everything it uses; so developing newer features was becoming a pain with newer and unsupported technologies.
So far we have managed to produce an almost fully working version of the system; but we got stuck at a feature which involves Datanucleus-JDO, MongoDB and inheritance.
We have some models which are tremendously similar (from the code's perspective). In the current in-production version, to apply a change to it usually involves to rewrite the same piece of code in all classes, so we thought that inheritance would make the job easier and better. So we have two interfaces at the top hierarchy level (which as far we know, Datanuclues nor MongoDB doesn't care about them at all); which go like this:
public interface Entity extends Serializable {
String getDate();
double getQty();
void setQty(double qty);
void setDate(String date);
void setKey(Key key);
}
And
public interface HourEntity extends Entity {
String getHour();
}
We use application defined keys, we use this unique class to build different kind of keys. We only want the toString representation of this class to store and retrieve data in Mongo.
public final class Key implements Serializable {
static final long serialVersionUID = -448150158203091507L;
public final String targetClassName;
public final String id;
public final String toString;
public final int hashCode;
public Key() {
targetClassName = null;
id = null;
toString = null;
hashCode = -1;
}
public Key(String str) {
String[] parts = str.split("\\(");
parts[1] = parts[1].replaceAll("\\)", " ");
parts[1] = parts[1].replace("\"", " ");
parts[1] = parts[1].trim();
this.targetClassName = parts[0];
this.id = parts[1];
toString = this.toString();
hashCode = this.hashCode();
}
public Key(String classCollectionName, String id) {
if (StringUtils.isEmpty(classCollectionName)) {
throw new IllegalArgumentException("No collection/class name specified.");
}
if (id == null) {
throw new IllegalArgumentException("ID cannot be null");
}
targetClassName = classCollectionName;
this.id = id;
toString = this.toString();
hashCode = this.hashCode();
}
public String getTargetClassName() {
return targetClassName;
}
public int hashCode() {
if(hashCode != -1) return hashCode;
int prime = 31;
int result = 1;
result = prime * result + (id != null ? id.hashCode() : 0);
result = prime * result + (targetClassName != null ? targetClassName.hashCode() : 0);
return result;
}
public boolean equals(Object object) {
if (object instanceof Key) {
Key key = (Key) object;
if (this == key)
return true;
return targetClassName.equals(key.targetClassName) && Objects.equals(id, key.id);
} else {
return false;
}
}
public String toString() {
if(toString != null) return toString;
StringBuilder buffer = new StringBuilder();
buffer.append(targetClassName);
buffer.append("(");
if (id != null) {
buffer.append((new StringBuilder()).append("\"").append(id)
.append("\"").toString());
} else {
buffer.append("no-id-yet");
}
buffer.append(")");
return buffer.toString();
}
}
This application defined identity is working fine on all other models which does not involve inheritance.
This is one of the actual models that we intend to store in our datastore:
#PersistenceCapable(detachable="true")
#Inheritance(strategy=InheritanceStrategy.COMPLETE_TABLE)
public class Ticket implements Entity {
#PrimaryKey
#Persistent(valueStrategy = IdGeneratorStrategy.UNSPECIFIED, column="_id")
protected Key key;
protected String date;
protected int qty;
public Ticket() {
this.qty = 0;
}
public Key getKey() {
return key;
}
#Override
public void setKey(Key key) {
this.key = key;
}
public double getQty() {
return qty;
}
public void setQty(double qty) {
this.qty = (int) qty;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((key == null) ? 0 : key.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;
Ticket other = (Ticket) obj;
if (key == null) {
if (other.key != null)
return false;
} else if (!key.equals(other.key))
return false;
return true;
}
#Override
public String toString() {
return "Ticket [key=" + key + ", date=" + date + ", qty="
+ qty + "]";
}
}
And this is its subclass (all models which involve this problem just involve one super class and only one children per every super class):
#PersistenceCapable(detachable="true")
#Inheritance(strategy=InheritanceStrategy.COMPLETE_TABLE)
public class HourTicket extends Ticket implements HourEntity {
private String hour;
public HourTicket() {
super();
}
public Key getKey() {
return key;
}
#Override
public void setKey(Key key) {
this.key = key;
}
public String getHour() {
return hour;
}
public void setHour(String hour) {
this.hour = hour;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((key == null) ? 0 : key.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;
HourTicket other = (HourTicket) obj;
if (key == null) {
if (other.key != null)
return false;
} else if (!key.equals(other.key))
return false;
return true;
}
#Override
public String toString() {
return "HourTicket [key=" + key + ", date=" + date
+ ", hour=" + hour + ", qty=" + qty + "]";
}
}
Finally, the persisntance.xml is like this
<?xml version="1.0" encoding="UTF-8" ?>
<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_2_0.xsd" version="2.0">
<!-- JOSAdmin "unit" -->
<persistence-unit name="ourdatastore">
<class>mx.ourdomain.Ticket</class>
<class>mx.ourdomain.HourTicket</class>
<exclude-unlisted-classes/>
</persistence-unit>
</persistence>
And package-mongo.orm
<?xml version="1.0"?>
<!DOCTYPE orm SYSTEM "file:/javax/jdo/orm.dtd">
<orm>
<package name="mx.ourdomain" >
<class name="Ticket" table="Ticket">
<field name="key" primary-key="true" >
<column name="_id" length="100" />
</field >
</class>
<class name="HourTicket" table="HourTicket">
<primary-key >
<column name="_id" target="_id" />
</primary-key>
</class>
</package>
</orm>
So, the problems comes when trying to perform any read or write operations using either the super class or the subclass. This has happened with the same exact results in several (all possible as far we know) scenarios, but the test scenario we are study begins with this call:
Ticket ticket = persistenceManager.getObjectById(Ticket.class, key);
The key is generated with an standard procedure which is used by other models which do store and read successfully; and of course, it is of the previously shown key class.
We have gone as far as debugging the datanucleus tasks beyond this. And we have found that as expected:
The metadata shows that its the super class of others.
Its using application managed keys.
But when trying to get the class name to determine which is the correct Mongo collection to query, datanucleus-mongodb tries to query both classes (Ticket and HourTicket); but then it handles to the mongo driver the key object perse, and then a CodecConfigurationException is thrown since mongo does not know how to work with the key class (when building the query, datanucleus-mongo creates a BasicDBObject which has the structure {_id:key}, which cannot be constructed without the codec because of the key entry. This happens at the MongoDBUtils class in the datanucleus-mongodb project v5.1.0; class MongoDBUtils, method getClassNameForIdentity(Object, AbstractClassMetaData, ExecutionContext, ClassLoaderResolver)).
So, we suppose that we have some configuration missing to tell datanucleus that it should use the toString() form of the key; since the Monogo driver handles String just fine (datanuclues docs actually states that when using custom classes as datastore keys it will use the toString() form of the key; so I'm unsure if this could be a bug).
We have tried to use a KeyTraslator plugin and making the key class a DatastoreId and wrapping in a StringId with no success: the same exception is fired, except when wrapping the Key class in a StringId: the mongo lecture is sucessful but then when trying to build the model object, an ClassCastException is thrown since String cannot be casted into Key, and refactoring the code to use a String key will badly break data already in database; since it has a special format the key class can read and produce.
Is there something we are missing using inheritance with datanucleus JDO w/mongoDB?
I was not putting much attention to the settings around the objectIdClass metadata; since from the docs I got that they were intended for composed keys only. It results that if you define an objectId class with only one attribute; then it behaves as a custom SingleFieldId; which is what we wanted.
I found "funny" the fact that non annotated (or non declared metadata for objectIdClass) classes will work fine and the custom key used will be threated just fine; but once you make any of them a super class, then you are obligated to add the objectIdClass metadata.
Beside annotating the Ticket class (and all other super classes) with objectIdClass, we:
Removed the toString and hashCode attributes from the Key class (#NotPersistent and transient keyword won't make Datanucleus ignore them; so I guess there is no performance improvement for toString() and hashCode() methods on custom keys right now).
Removed all the final qualifiers from the Key class attributes (Datanucleus docs don't say that custom key fields cannot be final; but guess what, they can't be)
Changed the Key key class member from all superclass for String id (as in the key class). We also had to change the implementation of the getters and setters for the id member; using the required string constructor of the key class to build the key when calling the method. Of course, the "key" field declared in the package-mongo.orm was changed to id in the super classes.
And that was it! with those little changes our system is working great; no other changed were required on other persistable classes nor DAOs.
Related
I'm encountering a (compatibility) problem with openapi, the java code generator (openapi-generator-maven-plugin. version: 6.0.1) and nullable properties.
I have defined an object which I use for PATCH endpoints and which resembles a picture and it's defined as a byte array byte[]. The openapi.yaml looks like this:
TestObject:
type: object
properties:
picture:
type: string
format: byte
nullable: true
The generated code looks like this:
#Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-09-08T11:52:30.791139+02:00[Europe/Malta]")
public class TestObject {
#JsonProperty("picture")
private JsonNullable<byte[]> picture = JsonNullable.undefined();
public TestObject picture(byte[] picture) {
this.picture = JsonNullable.of(picture);
return this;
}
/**
* Get picture
* #return picture
*/
#Schema(name = "picture", required = false)
public JsonNullable<byte[]> getPicture() {
return picture;
}
public void setPicture(JsonNullable<byte[]> picture) {
this.picture = picture;
}
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TestObject testObject = (TestObject) o;
return Arrays.equals(this.picture, testObject.picture);
^--------- no suitable method found for equals(org.openapitools.jackson.nullable.JsonNullable<byte[]>,org.openapitools.jackson.nullable.JsonNullable<byte[]>)
}
private static <T> boolean equalsNullable(JsonNullable<T> a, JsonNullable<T> b) {
return a == b || (a != null && b != null && a.isPresent() && b.isPresent() && Objects.deepEquals(a.get(), b.get()));
}
#Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(picture));
}
private static <T> int hashCodeNullable(JsonNullable<T> a) {
if (a == null) {
return 1;
}
return a.isPresent() ? Arrays.deepHashCode(new Object[]{a.get()}) : 31;
}
#Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("class TestObject {\n");
sb.append(" picture: ").append(toIndentedString(picture)).append("\n");
sb.append("}");
return sb.toString();
}
/**
* Convert the given object to string with each line indented by 4 spaces
* (except the first line).
*/
private String toIndentedString(Object o) {
if (o == null) {
return "null";
}
return o.toString().replace("\n", "\n ");
}
}
The problem is that in the equals method (see my error added inline), the generator uses Arrays.equals instead of Objects.equals for comparing the JsonNullable<byte[]> objects.
Any ideas of how to fix this? I tried a different approaches/searched the internet and didn't find anything that would help my situation.
Edit:
Seems like this issue was fixed in openapi-generator 5.2.1 https://github.com/OpenAPITools/openapi-generator/pull/10012
but I am using 6.0.1 at this moment and still encounter this.
I've also tried to add other nullable properties in my yaml definition file and I see that the generated method equalsNullable is never used, Objects.equals(a, b) is always the one that is being picked up.
I have a DAOImplementation class with the method definition below.
#Override
public Registration getRegistrationInfoById(int aRegistrationId) {
String SQL = "{CALL getRegistrationInfoById(?)}";
Registration aRegistration = new Registration();
try (Connection con = DBUtil.getConnection(DBType.MYSQL);
CallableStatement cs = con.prepareCall(SQL);) {
cs.setInt(1, aRegistrationId);
try (ResultSet rs = cs.executeQuery();) {
while (rs.next()) {
int gradeLevel = Integer.parseInt(rs.getString(RegistrationTable.GRADELEVEL));
aRegistration.setGradeLevel(gradeLevel);
}
}
} catch (SQLException e) {
JOptionPane.showMessageDialog(null, e.getErrorCode() + "\n" + e.getMessage());
}
return aRegistration;
}//end of method
This returns an integer value of Grade Level (1,2,3,4,5,6,7...so on...) which I've verified because I tried printing the output returned by aRegistration.getGradeLevel();
Now my problem is with my JComboBox. I have set a ListCellRenderer for my JComboBox which holds all the GradeLevel values
public class JComboBoxRenderer_GradeLevel extends JLabel implements ListCellRenderer<Object> {
public JComboBoxRenderer_GradeLevel() {
this.setOpaque(true);
}
#Override
public Component getListCellRendererComponent(JList<? extends Object> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
if (value instanceof GradeLevel) {
this.setText("" + ((GradeLevel) value).getGradelevel());
} else {
this.setText("--");
}
if (isSelected) {
this.setBackground(Color.YELLOW);
this.setForeground(list.getSelectionForeground());
} else {
this.setBackground(list.getBackground());
this.setForeground(list.getForeground());
}
return this;
}
}
And looks like this JComboBox as expected. (GradeLevel model is renderered to simply show an int value of gradelevel), ((GradeLevel) value).getGradelevel());returns an integer value.
I understand that even when JComboBox has its renderer that displays an integer value of GradeLevel by using ((GradeLevel)value).getGradeLevel(), the actual value on the JComboBox is still treated as instance of GradeLevel or object. But not a String or int.
So my problem is when I try to set the selected value to an int value, it won't change the selected value of the JComboBox. Nothing happens when I use setSelectedItem();
This is what I tried to do for the GUI.
//Grade Level
GradeLevelDaoImpl gldi = new GradeLevelDaoImpl();
List<GradeLevel> gradeLevels = gldi.getAllGradeLevelsInfo();
DefaultComboBoxModel gradeLevelModel = new DefaultComboBoxModel(gradeLevels.toArray());
jcmbGradeLevel.setModel(gradeLevelModel);
jcmbGradeLevel.setRenderer(new JComboBoxRenderer_GradeLevel());
jcmbGradeLevel.setSelectedIndex(-1);
GradeLevel gradeLevel = new GradeLevel();
gradeLevel.setGradelevel(registration.getGradeLevel());
jcmbGradeLevel.setSelectedItem(gradeLevel); //PROBLEM HERE, it doesn't change
JOptionPane displays this.
JOptionPane.showMessageDialog(null,"GradeLevel: "+gradeLevel);
JOptionPane.showMessageDialog(null,"GradeLevel: "+gradeLevel.getGradeLevel());
It doesn't seem to be able to compare the object I'm trying to set it to(gradeLevel) with the objects JComboBox has(gradeLevels). Notice the singular and plural.
How do I manipulate the types so that setSelectedItem() will match with what the JComboBox have?
Thanks.
If you want to do this by using different instances of the object, but with the same properties, then you need to override the class's equals and hashcode methods, so that the combination of properties are unique. This is very important, this is a relationship expectation that any object which is equal to another will have the same hashcode
This is a really quick example and I used by IDE's auto generation process (because I'm lazy), but, if your Registration class has other properties which should be considered when comparing to instances of the class, you will need to modify it to support them (again, any good IDE should be able to do this)
public class Registration {
private int gradeLevel;
public Registration(int gradeLevel) {
this.gradeLevel = gradeLevel;
}
public int getGradeLevel() {
return gradeLevel;
}
public void setGradeLevel(int gradeLevel) {
this.gradeLevel = gradeLevel;
}
#Override
public int hashCode() {
int hash = 7;
hash = 73 * hash + this.gradeLevel;
return hash;
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Registration other = (Registration) obj;
if (this.gradeLevel != other.gradeLevel) {
return false;
}
return true;
}
}
Then using something like...
Registration a = new Registration(1);
Registration b = new Registration(1);
Registration c = new Registration(2);
System.out.println(a.equals(b));
System.out.println(a.equals(c));
System.out.println(b.equals(c));
will print...
true
false
false
which shows us that the code is working.
Once you get this setup, you should then be able to change the selected item by creating an instance of Registration, seeding it with the required properties and passing it to the JComboBox.
This is very important and very common concept used a lot within Java, well worth taking the time to learn and understand
EDIT: Sample project available on github.
I'm using Neo4J (Rest graph database, hosted in grapheneDb) and Spring Data in our backend project.
<bean id="graphDatabaseService" class="org.springframework.data.neo4j.rest.SpringCypherRestGraphDatabase">
I have a simple one-to-many relationship between two entities: User and Stay.
EDIT: I thought this wasn't relevant for the issue, but after seeing a similar problem in SDN4, I think I need to update the question (there is a basic #NodeEntity class, and both entities are extending this base class).
#NodeEntity
public abstract class BasicNodeEntity implements Serializable {
#GraphId
private Long nodeId;
}
public class User extends BasicNodeEntity {
#RelatedTo(type = "HAS_STAY", direction = Direction.OUTGOING)
Set<Stay> stays;
public void addStay(Stay stay) {
stays.add(stay);
}
}
public class Stay extends BasicNodeEntity {
#RelatedTo(type = "HAS_STAY", direction = Direction.INCOMING)
User user;
}
I'm unable to persist more than one stay. The first stay I add to the user is persisted correctly, but just the first one. The next stays added never persists, and I always retrieve the first one.
The method I use to create a new stay is:
#Autowired
Neo4jOperations template;
#Transactional
private void createStay(Stay stay, User user) throws Exception {
stay = template.save(stay);
user.addStay(stay);
template.save(user);
// If i evaluate user at this point, it contains both stays
// But if I retrieve the user from the repository, it just contains
// the first stay, the second one has not persisted.
}
EDIT: User modified is retrieved correctly through UserRepository.
public interface UserRepositoryCustom {}
public interface UserRepository extends GraphRepository<User>, UserRepositoryCustom {
User findById(String id);
}
User user = userRepository.findById(userId);
NOTE: I also tried to save through the repository interface instead of the Neo4jTemplate one, but I have the same problem.
Both entities are correctly saved in the neo4j database, it's just a persistence issue.
I think this should be quite easy, so I'm probably missing something..
Any help would be greatly appreciated.
Relevant versions:
<spring.version>4.0.5.RELEASE</spring.version>
<spring-data-neo4j.version>3.3.2.RELEASE</spring-data-neo4j.version>
There is another SO question with a very similar problem, but without response so far.
It is a tricky thing.
Your custom equals method causes two entities which have their node-id set but not yet their uuid-id set, to be equal so that when loading them into a set the set will only contain one.
Code in: RelationshipHelper
protected Set<Object> createEntitySetFromRelationshipEndNodes(Object entity, final MappingPolicy mappingPolicy, final Class<?> relatedType) {
final Iterable<Node> nodes = getStatesFromEntity(entity);
final Set<Object> result = new HashSet<Object>();
for (final Node otherNode : nodes) {
Object target = template.createEntityFromState(otherNode, relatedType, mappingPolicy);
result.add(target);
}
return result;
}
If you change your code to have an equals/hashcode in your BasicNode entity:
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BasicNodeEntity)) return false;
BasicNodeEntity that = (BasicNodeEntity) o;
if (nodeId != null) {
if (!nodeId.equals(that.nodeId)) return false;
} else {
if (that.nodeId != null) return false;
}
return true;
}
#Override
public int hashCode() {
return nodeId != null ? nodeId.hashCode() : 0;
}
so that entities that have only a nodeId set are comparable
and adapt the subclass methods
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof IdentifiableEntity)) return false;
IdentifiableEntity entity = (IdentifiableEntity) o;
//change
if (!super.equals(o)) return false;
if (id != null) {
if (!id.equals(entity.id)) return false;
} else {
if (entity.id != null) return false;
}
return true;
}
#Override
public int hashCode() {
//change
if (super.hashCode() != 0) return super.hashCode();
return id != null ? id.hashCode() : 0;
}
Then it works.
Going forward if you are working with Neo4j Server I recommend to you to check out SDN 4 RC2 instead which was released on Friday.
I have an entity which has some BIT fields into the database:
editable
needs_review
active
These fields are mapped against boolean fields in its Java class using Hibernate 3.6.9 version. That forces me to write an interface method for each List of entities I want to get:
List<Entity> listEditables();
List<Entity> listReviewNeeded();
List<Entity> listActives();
Or write a general interface method to achieve a combination of them:
List<Entity> listEntities(boolean editables, boolean reviewNeeded, boolean actives);
That second choice looks greater, but if I add another field in the future there will be a need to modify the interface itself (and every line of code coupled to it).
So I decided I can express it as an enumeration Set:
public enum EntityType{
EDITABLE, REVIEW_NEEDED, ACTIVE
}
//That way there's no need to change interface method's signature
List<Entity> listEntities(Set<EntityType> requiredTypes);
It makes sense that being an enumeration match what I want to achieve, the Entity type itself should have its own Set<EntityType>:
public class Entity{
Set<EntityType> entityTypes;
}
However instead of that I have the mapped booleans which logically match that Set. Then my question, is there any way to map Set<EntityType> entityTypes in hibernate based in that BIT fields or do I have to manage that logic myself having them as boolean?
UPDATE
Having them mapped as a Set implies the possibility of querying for a List using an in clause, if not it would imply an extra step for conversion between my controller and model codes.
Set<EntityType> typesSet = Sets.newHashSet(EntityType.EDITABLE, EntityType.REVIEW_NEEDED);
//Obtains a list of every single entity which is EDITABLE or REVIEW_NEEDED
session.createCriteria(Entity.class).addRestriction(Restrictions.in("entityTypes",typeSet)).list();
I think I have a solution for you. What you are interested in is a CompositeUserType.
As an example lets use a InetAddress composite user type I wrote lately to map a 128bit IPv6 Address / IPv4Address object to two 64bit long properties inside a user account entity.
The signupIp:InetAddress is mapped towards two columns (there is no column count limit or alike) using:
#Columns(columns = {#Column(name = "ip_low", nullable = true), #Column(name = "ip_high", nullable = true)})
private InetAddress signupIp;
And the interesting part of the implementation looks like this:
public class InetAddressUserType implements CompositeUserType {
#Override
public String[] getPropertyNames() {
return new String [] {"ipLow", "ipHigh"};
}
#Override
public Type[] getPropertyTypes() {
return new Type [] { LongType.INSTANCE, LongType.INSTANCE};
}
#Override
public Object getPropertyValue(Object component, int property) throws HibernateException {
if(component != null)
return toLong((InetAddress)component)[property];
else
return null;
}
#Override
public void nullSafeSet(PreparedStatement st, Object value, int index,
SessionImplementor session) throws HibernateException, SQLException {
if(value != null) {
long [] longs = toLong((InetAddress)value);
st.setLong(index, longs[0]);
st.setLong(index + 1, longs[1]);
}
else {
st.setNull(index, LongType.INSTANCE.sqlType());
st.setNull(index + 1, LongType.INSTANCE.sqlType());
}
}
#Override
public void setPropertyValue(Object component, int property, Object value)
throws HibernateException {
throw new RuntimeException("This object is immutable");
}
#Override
public Class<?> returnedClass() {
return InetAddress.class;
}
#Override
public boolean equals(Object x, Object y) throws HibernateException {
return x != null ? x.equals(y) : null == y;
}
#Override
public int hashCode(Object x) throws HibernateException {
return x.hashCode();
}
#Override
public Object nullSafeGet(ResultSet rs, String[] names,
SessionImplementor session, Object owner)
throws HibernateException, SQLException {
Long ipLow = rs.getLong(names[0]);
if(!rs.wasNull()) {
Long ipHigh = rs.getLong(names[1]);
try {
return fromLong(new long [] {ipLow, ipHigh});
} catch (UnknownHostException e) {
throw new HibernateException("Failed to get InetAddress: ip = " + ipHigh + " + " + ipLow, e);
}
}
else
return null;
}
#Override
public Object deepCopy(Object value) throws HibernateException {
if(value != null)
try {
return InetAddress.getByAddress(((InetAddress)value).getAddress());
} catch (UnknownHostException e) {
throw new RuntimeException("Impossible Exception: " + e.getMessage(), e);
}
else
return null;
}
#Override
public boolean isMutable() {
return false;
}
...
}
Note that I flexibly switch between Inet4Address and Inet6Address instances depending on the values of ipLow and ipHigh. The composite is marked as immutable and you need to check the documentation and the examples in the Hibernate source code (build in composite user types).
In a similar way you can map your meaningful bit properties. You can query those bits by using a single Restriction.eq refering to your EnumType. You can use the equals method to check the properties object. And if you need to refer to a special mapped bit you can use the dot notation like in signupIp.ipLow to refer to the ipLow property/column.
I guess this is what you are looking for.
Update:
In the end it boils down to define the right order of your properties. Hibernate will always use integer index values to access each property:
//immutable for simplicity
class Status {
private final boolean editable;
private final boolean needsReview;
private final boolean active;
//... constructor + isEditable etc..
}
In your StatusCompositeType class:
public String[] getPropertyNames() {
return new String [] {"editable", "needsReview", "active"};
}
public Type[] getPropertyTypes() {
return new Type [] { BooleanType.INSTANCE, LongType.INSTANCE};
}
public Object getPropertyValue(Object component, int property) throws HibernateException {
if(component != null) {
Status status = (Status)component;
switch(property) {
case 1: return status.isEditable();
case 2: return status.isReviewNeeded();
case 3: return status.isActive();
default: throw new IllegalArgumentException();
}
}
else
return null; //all columns can be set to null if you allow a entity to have a null status.
}
public void nullSafeSet(PreparedStatement st, Object value, int index,
SessionImplementor session) throws HibernateException, SQLException {
if(value != null) {
Status status = (Status)value;
st.setBoolean(index, status.isEditable());
st.setBoolean(index + 1, status.isReviewNeeded());
st.setBoolean(index + 2, status.isActive());
}
else {
st.setNull(index, BooleanType.INSTANCE.sqlType());
st.setNull(index + 1, BooleanType.INSTANCE.sqlType());
st.setNull(index + 2, BooleanType.INSTANCE.sqlType());
}
}
public Object nullSafeGet(ResultSet rs, String[] names,
SessionImplementor session, Object owner)
throws HibernateException, SQLException {
Boolean isEditable = rs.getBoolean(names[0]);
if(!rs.wasNull()) {
Boolean isReviewNeeded = rs.getBoolean(names[1]);
Boolean isActive = rs.getBoolean(names[2]);
return new Status(isEditable, isReviewNeeded, isActive);
}
else
return null;
}
The rest is straight forward. Remember to implement equals and hashcode for the user type and add the type to the configuration before you create your sessionFactory.
Once you have everything in place you can create a criteria search and use:
//search for any elements that have a status of editable, no reviewNeeded and is not active (true false false).
criteria.add(Restrictions.eq("status", new Status(true, false, false));
Now your listEntities method may become either: listEntities(Status status) or listEntities(boolean editable, boolean reviewNeeded, boolean isActive).
If you need additional information just check the CompositeType and BasicType implementations Hibernate provides within its own sourcecode (look for implementors of CompositeType and BasicType). Understanding those helps alot to use and learn this intermediate level knowledge of Hibernate.
After some brainstorming, I've gone to a workaround which I consider the second best one being imposible to map an enum for the booleans in Hibernate. This is how I have my Entity class looks now:
public class Entity{
private boolean editable;
private boolean needsReview;
private boolean active;
//getters and setters
}
My listing method is implemented as this:
public List<Entity> listEntities(Set<EntityType> requiredTypes){
Criteria cri = session.createCriteria(Entity.class);
if (requiredTypes.contains(EntityType.EDITABLE)){
cri.addRestriction(Restrictions.eq("editable",true));
}
if (requiredTypes.contains(EntityType.NEEDS_REVIEW)){
cri.addRestriction(Restrictions.eq("needsReview",true));
}
if (requiredTypes.contains(EntityType.ACTIVE)){
cri.addRestriction(Restrictions.eq("active",true));
}
return cri.list();
}
Not bad, but don't know if it's the only way to go with that!
I don't think hibernate provides a way to manage the mappings the way you're describing. You can create your own UserType (https://community.jboss.org/wiki/Java5EnumUserType) but every time you add a new enum value you will have to change the logic in the UserType to map the new field as well.
The alternative will be to convert this into a one to many relationship. Your point is basically that if you want to add more fields you will have to change the signature of listEntities but also you will have to modify your table.
So, instead you can create a table that will contain your entity types and have a #OneToMany` relationship to it from your entity. For example:
Define your flags as required:
public enum Flags {
EDITABLE, REVIEW_NEEDED, ACTIVE
}
Create a one-to-many relationship to EntityType:
#Entity
#Table( name="entity" )
public class Entity implements Serializable {
#OneToMany(mappedBy = "entity")
public Set<EntityType> getEntityTypes() {
return entityTypes;
}
And a many-to-one to Entity:
#Entity
#Table( name="entityType" )
public class EntityType implements Serializable {
#Id
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ENTITY_ID")
private Entity entity;
#Enumerated(EnumType.STRING)
private Flag entityType;
...
}
PD: Please note the code is just an example and is not complete or tested.
As in subject: Is it proper equals method for my class Java? I have generated it automaticly by Eclipse I don't know if it makes vector.remove(pracownik) will work correctrly. Or is it wrong to generate it by Eclipse?
import java.util.Date;
import java.util.Vector;
public class Pracownik extends Osoba {
private String stanowisko;
private int pensja;
private Date dataZatrudnienia;
public Pracownik(Adres adres, String telefon, String imie, String nazwisko,
int id, Date dataUrodzenia, String stanowisko, int pensja,
Date dataZatrudnienia) {
super(adres, telefon, imie, nazwisko, id, dataUrodzenia);
this.stanowisko = stanowisko;
this.pensja = pensja;
this.dataZatrudnienia = dataZatrudnienia;
}
public String getStanowisko() {
return stanowisko;
}
public int getPensja() {
return pensja;
}
public Date getDataZatrudnienia() {
return dataZatrudnienia;
}
#Override
public String toString() {
return super.toString() + "\nstanowisko=" + stanowisko + "\npensja="
+ pensja + "\ndataZatrudnienia=" + dataZatrudnienia;
}
private static Vector<Pracownik> ekstensja = new Vector<Pracownik>();//kolekcja zawierajaca ekstensje
private static void dodajPracownik(Pracownik pracownik) { //metoda dodajac aobiekt do ekstensji
ekstensja.add(pracownik);
}
private static void usunPracownik(Pracownik pracownik) {//metoda usuwajaca obiekt z ekstensji
ekstensja.remove(pracownik);
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Pracownik other = (Pracownik) obj;
if (dataZatrudnienia == null) {
if (other.dataZatrudnienia != null)
return false;
} else if (!dataZatrudnienia.equals(other.dataZatrudnienia))
return false;
if (pensja != other.pensja)
return false;
if (stanowisko == null) {
if (other.stanowisko != null)
return false;
} else if (!stanowisko.equals(other.stanowisko))
return false;
return true;
}
private static void pokazEkstensje(){ //wyswietlenie ekstensji przy pomocy petli for each
System.out.println("Ekstensja klasy Pracownik");
for(Pracownik pracownik: ekstensja)
System.out.println(pracownik);
System.out.println();
}
public static void main(String[] args){
Adres adres = new Adres("tara", "588 m.42", "03-422", "Warszawa");
Pracownik pracownik = new Pracownik(adres, "02-6451-4564", "Ala", "Kotowa", 323, new Date(), "szef", 14000, new Date()); //tworze pracownika
System.out.println(pracownik);//wyswietlam pracowanika
//tworze stazyste
Stazysta stazysta = new Stazysta(adres, "3232 9898", "frajer", "costam", 3232, new Date(), "podawanie kawy", 0, new Umowa(new Date(2010,10,5), new Date(2011,11,8)));
//wysswietlam stazyste
System.out.println(stazysta);
}
}
Generating the equals method using Eclipse is fine. It is important to make sure a field is included in the generation if, and only if, it affects logical equality of your objects.
When you override equals, you should also override hashCode.
In general, when overriding inherited methods you need to ensure the new methods conform to any rules stated in the superclass. The Object hashCode documentation states several rules, including "If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result."
The hashCode you inherit from Object follows that rule - if the equals method is also the one inherited from Object. It does not follow that rule when used with your equals method.
If you let Eclipse do its "Generate hashCode() and equals()" thing, it will get it right. If you write an equals method manually, you need to write your own hashCode to match.
As a practical matter, a class that does not follow the Object hashCode contract is a trap for future reuse. Hashed data structures, such as HashMap and HashSet, may fail to find an object that is actually present if it has a broken hashCode method. One lesson I've learned the hard way is that it is a mistake to depend on "I'll never use this that way.". It is much better to keep things safe as one goes along.
I am not expert on this but I believe double equals are comparing address rather than content of a variable. So, you may change it to .equals(). I hope someone with much more experience correct me if I am wrong.