I try to understand the purpose of #ShallowReference by creating a test case but I don't see any differences in the Javers changes. My application is a Spring Boot application with repository annotation.
I have a one to many bidirectional relationship between Customer and Project. The test case creates a Customer and a Project, then the project is added to the customer. I tried to run the test case with and without the #ShallowReference on the Set<Project> field to see the #ShallowReference behaviour. Then I query the changes but they are the same in both cases. I am looking for a test case to understand the #ShallowReference.
Test case:
#RunWith(SpringRunner.class)
#DataJpaTest
#Import({JaversSqlAutoConfiguration.class})
public class AuditTest {
#Autowired
private ProjectRepository projectRepository;
#Autowired
private Javers javers;
#Autowired
private CustomerRepository customerRepository;
#Test
public void testJavers() {
Customer customer = new Customer();
customer.setName("Stackoverflow");
customerRepository.save(customer);
Project project = new Project();
project.setName("Framework");
customer.addProject(project);
projectRepository.save(project);
QueryBuilder jqlQuery = QueryBuilder.anyDomainObject().withNewObjectChanges();
List<Change> changes = javers.findChanges(jqlQuery.build());
assertEquals(9, changes.size());
project.setName("Backend Framework");
projectRepository.save(project);
changes = javers.findChanges(jqlQuery.build());
assertEquals(10, changes.size());
}
}
Customer entity:
#Entity
#Table(name = "CUSTOMER")
#SQLDelete(sql = "UPDATE customer SET deleted = true WHERE id = ?")
#Where(clause = "deleted = false")
public class Customer extends AbstractEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String name;
#OneToMany(mappedBy = "customer")
// #ShallowReference
private Set<Project> projects = new HashSet<>();
// Getter/Setter
public void addProject(Project project) {
projects.add(project);
project.setCustomer(this);
}
public void removeProject(Project project) {
projects.remove(project);
project.setCustomer(null);
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Customer customer = (Customer) o;
return Objects.equals(id, customer.id);
}
#Override
public int hashCode() {
return Objects.hash(id);
}
}
Project entity:
#Entity
#Table(name = "PROJECT")
#SQLDelete(sql = "UPDATE project SET deleted = true WHERE id = ?")
#Where(clause = "deleted = false")
#EntityListeners(AuditingEntityListener.class)
public class Project extends AbstractEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String name;
#ManyToOne(fetch = FetchType.EAGER)
private Customer customer;
// Getter/Setter
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Project)) return false;
Project project = (Project) o;
return Objects.equals(id, project.id);
}
#Override
public int hashCode() {
return Objects.hash(id);
}
}
#ShallowReference doesn't work for collections. We have an open issue for that https://github.com/javers/javers/issues/528
PR's are welcome.
Related
I am trying to use the JPA Criteria API to filter the results and aggregate them using simple count, min, avg and max. I am using Spring Boot 2.7.8, so I am trying to use Interface-projections such that these aggregated results look the same as the simpler queries done automatically by the Spring repositories.
My domain entity (simplified for brevity) looks like this:
#Entity
#Table(name = "vehicle_stopped")
#IdClass(VehicleStopped.VehicleStoppedPK.class)
public class VehicleStopped implements Serializable {
#Id
#Column(name = "stopped_session_uuid", nullable = false)
private String stoppedSessionUuid;
#Id
#Column(name = "start_ts", nullable = false)
private OffsetDateTime startTs;
#Column(name = "end_ts", nullable = false)
private OffsetDateTime endTs;
#Column(name = "duration_seconds")
private Double durationSeconds;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "zone_id")
private CameraZone cameraZone;
#Override
public VehicleStoppedPK getId() {
VehicleStopped.VehicleStoppedPK pk = new VehicleStopped.VehicleStoppedPK();
pk.setStartTs(this.getStartTs());
pk.setStoppedSessionUuid(this.getStoppedSessionUuid());
return pk;
}
public OffsetDateTime getEndTs() {
return endTs;
}
public void setEndTs(OffsetDateTime endTs) {
this.endTs = endTs;
}
public Double getDurationSeconds() {
return durationSeconds;
}
public void setDurationSeconds(Double durationSeconds) {
this.durationSeconds = durationSeconds;
}
public CameraZone getCameraZone() {
return cameraZone;
}
public void setCameraZone(CameraZone cameraZone) {
this.cameraZone = cameraZone;
}
public VehicleType getVehicleType() {
return vehicleType;
}
public void setVehicleType(VehicleType vehicleType) {
this.vehicleType = vehicleType;
}
public String getStoppedSessionUuid() {
return stoppedSessionUuid;
}
public void setStoppedSessionUuid(String stoppedSessionUuid) {
this.stoppedSessionUuid = stoppedSessionUuid;
}
//some details removed for brevity
#Override
public static class VehicleStoppedPK implements Serializable {
private OffsetDateTime startTs;
private String stoppedSessionUuid;
public VehicleStoppedPK() {
}
public OffsetDateTime getStartTs() {
return startTs;
}
public void setStartTs(OffsetDateTime startTs) {
this.startTs = startTs;
}
public String getStoppedSessionUuid() {
return stoppedSessionUuid;
}
public void setStoppedSessionUuid(String stoppedSessionUuid) {
this.stoppedSessionUuid = stoppedSessionUuid;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
VehicleStoppedPK that = (VehicleStoppedPK) o;
return Objects.equals(startTs, that.startTs) && Objects.equals(stoppedSessionUuid, that.stoppedSessionUuid);
}
#Override
public int hashCode() {
return Objects.hash(startTs, stoppedSessionUuid);
}
#Override
public String toString() {
return "VehicleStoppedPK{" +
"startTs=" + startTs +
", stoppedSessionUuid='" + stoppedSessionUuid + '\'' +
'}';
}
}
}
#Entity
#Table(name = "camera_zone")
public class CameraZone implements Serializable {
#Id
#SequenceGenerator(name = "camera_zone_id_seq", sequenceName = "camera_zone_id_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "camera_zone_id_seq")
#Column(name = "id", updatable=false)
private Integer id;
#Column(name = "uuid", unique = true)
private String uuid;
#Column(name = "type")
private String type;
#Column(name = "name")
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CameraZone that = (CameraZone) o;
return Objects.equals(id, that.id) && Objects.equals(uuid, that.uuid) && Objects.equals(camera, that.camera) && Objects.equals(type, that.type) && Objects.equals(name, that.name);
}
#Override
public int hashCode() {
return Objects.hash(id, uuid, camera, type, name);
}
}
The code that I have in my Repository implementation looks like this:
public class SpecificationVehicleStoppedRepositoryImpl
implements SpecificationVehicleStoppedRepository {
#Autowired private EntityManager em;
#Autowired ProjectionFactory projectionFactory;
#Override
public List<VehicleStoppedAggregate> getStoppedVehiclesCount(Specification<VehicleStopped> spec) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Tuple> query = builder.createTupleQuery();
Root<VehicleStopped> root = query.from(VehicleStopped.class);
Predicate predicate = spec.toPredicate(root, query, builder);
if (predicate != null) {
query.where(predicate);
}
Path<Number> duration = root.get("durationSeconds");
Path<CameraZone> zone = root.get("cameraZone");
query
.multiselect(zone,
builder.count(root).alias("totalVehicles"),
builder.min(duration).alias("minDuration"),
builder.avg(duration).alias("avgDuration"),
builder.max(duration).alias("maxDuration"))
.groupBy(zone);
List<Tuple> rawResultList = em.createQuery(query).getResultList();
return project(rawResultList, VehicleStoppedAggregate.class);
}
private <P> List<P> project(List<Tuple> results, Class<P> projectionClass) {
return results.stream()
.map(tuple -> {
Map<String, Object> mappedResult = new HashMap<>(tuple.getElements().size());
for (TupleElement<?> element : tuple.getElements()) {
String name = element.getAlias();
mappedResult.put(name, tuple.get(name));
}
return projectionFactory.createProjection(projectionClass, mappedResult);
})
.collect(Collectors.toList());
}
}
The interface-based projection I am trying to populate (using SpelAwareProxyProjectionFactory) is this:
public interface VehicleStoppedAggregate {
CameraZone getCameraZone();
Integer getTotalVehicles();
Double getMinDuration();
Double getAvgDuration();
Double getMaxDuration();
}
The call to getStoppedVehiclesCount() fails with the following error:
ERROR: column "camerazone1_.id" must appear in the GROUP BY clause or be used in an aggregate function
This error is coming from the PostgreSQL database, and rightly so because the SQL hibernate generates is incorrect:
select
vehiclesto0_.zone_id as col_0_0_,
count(*) as col_1_0_,
min(vehiclesto0_.duration_seconds) as col_2_0_,
avg(vehiclesto0_.duration_seconds) as col_3_0_,
max(vehiclesto0_.duration_seconds) as col_4_0_,
camerazone1_.id as id1_2_,
camerazone1_.name as name2_2_,
camerazone1_.type as type3_2_,
camerazone1_.uuid as uuid4_2_
from
vehicle_stopped vehiclesto0_
inner join
camera_zone camerazone1_
on vehiclesto0_.zone_id=camerazone1_.id cross
where
vehiclesto0_.start_ts>=?
and vehiclesto0_.start_ts<=?
and 1=1
and 1=1
and 1=1
group by
vehiclesto0_.zone_id
It is not grouping by the other fields it is requesting from the joined table.
If I had to use a normal class, instead of a Tuple, it would work, but it would mean I would have to create a class with a huge constructor for all fields for Hibernate to populate it.
Somehow, when I use Interface-based projections with Spring's repositories rather than my criteriaquery, the same scenario works. They manage to populate the one-to-many relationships just fine.
Is there a way to fix this and make Hibernate ask for the right fields?
I am using Hibernate 5.6.14.Final (as bundled with Spring Boot 2.7.8).
I believe the "solution" is two create two "independent" query roots and join them together:
CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<Tuple> query = builder.createTupleQuery();
Root<VehicleStopped> root = query.from(VehicleStopped.class);
// instead of Path<CameraZone> zone = root.get("cameraZone")
Root<CameraZone> zone = query.from(CameraZone.class);
query.where(builder.equal(zone, root.get("cameraZone")));
Path<Number> duration = root.get("durationSeconds");
query
.multiselect(zone,
builder.count(root).alias("totalVehicles"),
builder.min(duration).alias("minDuration"),
builder.avg(duration).alias("avgDuration"),
builder.max(duration).alias("maxDuration"))
.groupBy(zone);
session.createQuery(query).getResultList();
In that case Hibernate 5 produces following SQL (which actually looks weird from my perspective due to missing columns in group by clause):
select
naturalidc1_.id as col_0_0_,
count(*) as col_1_0_,
min(naturalidc0_.duration_seconds) as col_2_0_,
avg(naturalidc0_.duration_seconds) as col_3_0_,
max(naturalidc0_.duration_seconds) as col_4_0_,
naturalidc1_.id as id1_0_,
naturalidc1_.name as name2_0_,
naturalidc1_.type as type3_0_,
naturalidc1_.uuid as uuid4_0_
from
vehicle_stopped naturalidc0_ cross
join
camera_zone naturalidc1_
where
naturalidc1_.id=naturalidc0_.zone_id
group by
naturalidc1_.id
FYI. Your initial query does work in Hibernate 6 and produced SQL does look more correct but still weird:
select
c1_0.id,
c1_0.name,
c1_0.type,
c1_0.uuid,
count(*),
min(v1_0.duration_seconds),
avg(v1_0.duration_seconds),
max(v1_0.duration_seconds)
from
vehicle_stopped v1_0
join
camera_zone c1_0
on c1_0.id=v1_0.zone_id
group by
1,
2,
3,
4
So,
I am trying to do a many to many implementation through Spring JPA.
I tried to do an embedded key implementation.
But I am getting this error
{
"message": "Internal server error",
"details": "Could not set field value [26] value by reflection : [class com.domain.configuredview.model.RelationshipViewPersonPK.personId] setter of com.domain.configuredview.model.RelationshipViewPersonPK.personId; nested exception is org.hibernate.PropertyAccessException: Could not set field value [26] value by reflection : [class com.domain.configuredview.model.RelationshipViewPersonPK.personId] setter of com.domain.configuredview.model.RelationshipViewPersonPK.personId"
}
I tried searching through all the answers but couldn't fix this.
My classes are
import com.domain.person.model.Person;
import java.io.Serializable;
import javax.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
#Data
#NoArgsConstructor
#Entity
#Table(name = "relationship_view_person")
public class RelationshipViewPerson implements Serializable {
#EmbeddedId
private RelationshipViewPersonPK entryId;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("viewId")
#JoinColumn(name = "view_id", nullable = false)
private ConfiguredView view;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("personId")
#JoinColumn(name = "person_id", nullable = false)
private Person person;
public RelationshipViewPerson(ConfiguredView view, Person person) {
this.view = view;
this.person = person;
this.entryId = new RelationshipViewPersonPK(view.getId(), person.getWsGlobalId());
}
public void setEntryId(Long viewId, String personId) {
this.entryId.setViewId(viewId);
this.entryId.setPersonId(personId);
}
public void setView(ConfiguredView view) {
this.view = view;
}
public void setPerson(Person person) {
this.person = person;
}
public Person getPerson() {
return this.person;
}
public ConfiguredView getView() {
return this.view;
}
}
The relationship key is
import java.io.Serializable;
import javax.persistence.*;
import lombok.NoArgsConstructor;
#Embeddable
#NoArgsConstructor
public class RelationshipViewPersonPK implements Serializable {
#Column(name = "view_id")
private Long viewId;
#Column(name = "person_id")
private String personId;
public RelationshipViewPersonPK(Long viewId, String personId) {
this.viewId = viewId;
this.personId = personId;
}
// setters & getters
public void setViewId(Long viewId) {
this.viewId = viewId;
}
public void setPersonId(String personId) {
this.personId = personId;
}
public String getPersonId() {
return this.personId;
}
public Long getViewId() {
return this.viewId;
}
// Override equals and hashCode
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RelationshipViewPersonPK that = (RelationshipViewPersonPK) o;
return this.viewId.equals(that.viewId) && this.personId.equals(that.personId);
}
#Override
public int hashCode() {
return this.viewId.hashCode() + this.personId.hashCode();
}
}
The ConfiguredView model has PK as id and Person model has field as wsGlobalId . The key on the Person table is not a PK.
I have the setters, getters, constructors, and pretty much everyting in place. Not sure whats happening with Hibernate here, it cannot find the getter or something? Not sure whats happening with this.
You should have a setter like below:
public void setEntryId(RelationshipViewPersonPK entryId) {
this.entryId.setViewId(entryId.getViewId());
this.entryId.setPersonId(entryId.getPersonId());
}
instead of (or in addition to) this:
public void setEntryId(Long viewId, String personId) {
this.entryId.setViewId(viewId);
this.entryId.setPersonId(personId);
}
As the title suggests: I am currently migrating from Hibernate 3.6 to 4.2.
PROBLEM
After the upgrade, Hibernate started to automatically generate a foreign key that points to the same table. Now, when trying to persist a new customer, a ConstraintViolationException is thrown.
DETAILS
Here's a picture of what I mean:
Here is the code of the corresponding class:
#Entity
#Table(name = "customers")
#Inheritance(strategy = InheritanceType.JOINED)
public abstract class Customer extends AbstractModel {
public Customer(final Provider provider) {
this.provider = provider;
}
#Required
#ManyToOne(cascade = CascadeType.REFRESH)
public Provider provider;
#MaxSize(1023)
public String note;
public String getNote() { return note; }
...
}
As you can see, it extends from AbstractModel which is a #MappedSuperclass and contains the id property which serves as a primary key for all of our model classes. It's a #GeneratedValue.
It's a superclass itself. I don't know whether it's important so I am just going to include the code and schema of one of its child classes:
And the corresponding code:
#Entity
#Table(name="unregistered_customers")
public class UnregisteredCustomer extends Customer {
#MaxSize(MAX_SIZE_OF_NAMES_AND_IDENTIFIERS)
#Column(nullable = false, length = MAX_SIZE_OF_NAMES_AND_IDENTIFIERS)
public String userName;
#Email
#MaxSize(MAX_SIZE_OF_EMAIL_ADDRESSES)
#Column(unique = false, length = MAX_SIZE_OF_EMAIL_ADDRESSES)
public String email;
#MaxSize(MAX_SIZE_OF_NAMES_AND_IDENTIFIERS)
#Column(length = MAX_SIZE_OF_NAMES_AND_IDENTIFIERS)
public String mobile;
public UnregisteredCustomer(final Provider provider) {
super(provider);
}
RESEARCH
I already looked into the migration guide but nothing in there seems to be related to my issue.
I also looked into the Hibernate documentation especially into the section dealing with #Inheritance. Sadly, I didn't find anything concerning auto generated foreign keys.
QUESTION
How do I stop Hibernate from adding this self-referencing foreign key?
EDIT 1
As requested, the super class:
#MappedSuperclass
public abstract class AbstractModel extends AbstractBaseModel {
#Id
#GeneratedValue
public Long id;
public Long getId() {
return id;
}
#Override
public Object _key() {
return id;
}
}
Also, the super class of the super class:
#MappedSuperclass
public abstract class AbstractBaseModel extends GenericModel {
public static final int MAX_SIZE_OF_NAMES_AND_IDENTIFIERS = 80;
public static final int MAX_SIZE_OF_COMMENTS_AND_DESCRIPTIONS = 5000;
public static final int MAX_LIST_SIZE = 30;
public static final int MAX_SIZE_OF_EMAIL_ADDRESSES = 255;
public static final int MAX_SIZE_OF_JSON_CONTENT_FIELDS = 65535;
#Column(nullable = false, unique = true)
public String uuid;
#Column(nullable = false)
public Long created;
public DateTime getCreatedAsDate() {
return DateUtil.dateTimeWithSystemTimezone(created);
}
#Column(nullable = false)
public Long lastModified;
#PrePersist
protected void prePersist() {
final Long nowInMillis = DateUtil.dateTimeWithSystemTimezone().getMillis();
if (uuid == null) {
uuid = UuidUtil.newUUIDAsString();
Logger.trace("Created new Uuid for entity: %s", uuid);
}
if (created == null) {
created = nowInMillis;
}
if (lastModified == null) {
lastModified = nowInMillis;
}
}
#PreUpdate
protected void preUpdate() {
lastModified = DateUtil.dateTimeWithSystemTimezone().getMillis();
}
#Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
final AbstractBaseModel other = (AbstractBaseModel) obj;
return new EqualsBuilder().append(uuid, other.uuid).isEquals();
}
#Override
public int hashCode() {
return new HashCodeBuilder().append(uuid).toHashCode();
}
}
Put #Inheritance(strategy = InheritanceType.JOINED) to the parent entity (AbstractModel) not to the child. And customize the foreign key constraint using #PrimaryKeyJoinColumn at your child entity. Check out Part 4 (Joined Table) at https://www.baeldung.com/hibernate-inheritance to see it more clearly!
I have following dao method:
#Override
public List<AdminRole> findAll() {
Session session = sessionFactory.getCurrentSession();
Criteria criteria = session.createCriteria(AdminRole.class);
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
return criteria.list();
}
Actually I want to retrieve all entries from database.
Sometimes I see duplicates. This happens when I add user with AdminRole.
I have read that it is possible when I use EAGER fetch type and this should be fix adding following line:
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
But this doesn't help me.
my mapping:
#Entity
#Table(name = "terminal_admin_role")
public class AdminRole {
#Id
#Column(name = "role_id", nullable = false, unique = true)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_id")
#SequenceGenerator(name = "user_id", sequenceName = "user_id")
private Long adminId;
#Column(name = "role")
private String role;
public AdminRole(String role) {
this.role = role;
}
public AdminRole() {
}
// get set
#Override
public String toString(){
return role;
}
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AdminRole)) {
return false;
}
AdminRole adminRole = (AdminRole) o;
if (!role.equals(adminRole.role)) {
return false;
}
return true;
}
#Override
public int hashCode() {
return role.hashCode();
}
}
and
#Entity
#Table(name = "terminal_admin")
public class TerminalAdmin {
#ManyToMany(fetch=FetchType.EAGER,cascade=CascadeType.ALL)
#JoinTable(name = "admin_role", joinColumns = {
#JoinColumn(name = "admin_id", nullable = false) },
inverseJoinColumns = { #JoinColumn(name = "role_id",
nullable = false) })
private Set<AdminRole> adminRoles;
//...
}
P.S.
I cannot switch fetch type.
I don't want to put this list into set.
There is no reason to use DISTINCT_ROOT_ENTITY or anything similar, all you need is:
session.createCriteria(AdminRole.class).list();
If you get duplicates, then you really have them in the database. Check the code which saves AdminRoles either directly or by cascading from other entities.
When cascading PERSIST/MERGE operations from other entities, make sure that the operation is cascaded to a persistent/detached AdminRole instance, not to a transient (new) one.
My money's on messing with hashCode/equals overrides and Hibernate proxies.
From EqualsandHashCode
However, once you close the Hibernate session, all bets are off. [...] Hence, if you keep collections of objects around between sessions, you will start to experience odd behavior (duplicate objects in collections, mainly).
First I'd use org.apache.commons.lang3 like so (it's obviously way too expensive for Hibernate entities but works ok w/ them and if it works that should validate my hunch):
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
#Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
#Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
#Override
public boolean equals(Object other) {
return EqualsBuilder.reflectionEquals(this, other);
}
If this works you could go with a less expensive approach like so:
#Override
public int hashCode() {
HashCodeBuilder hcb = new HashCodeBuilder();
hcb.append(role);
return hcb.toHashCode();
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof AdminRole)) {
return false;
}
AdminRole that = (AdminRole) obj;
EqualsBuilder eb = new EqualsBuilder();
eb.append(role, that.role);
return eb.isEquals();
}
I'm facing a problem with Ebean when trying to create Composed Keys (EmbeddedId).
This is a draft of what I have:
#Entity public class EntityA extends Model{
#Id
private String ID;
#OneToMany
private List<EntityB> listEntitesB;
public EntityA(){
ID = UUID.randomUUID();
}
}
#Entity public class EntityB extends Model{
#EmbeddedId
private EntityB_PK ID;
#ManyToOne
#JoinColumn(name="entityA_fk", referencedColumnName="listEntitiesB")
private EntityA entityA;
public EntityB(String entityB_ID){
ID = new EntityB_PK(UUID.randomUUID(), entityB_ID);
}
}
#Embeddable public class EntityB_PK{
private String entityB_ID;
private String entityA_FK_ID;
public EntityB_PK(String entB_ID, String entA_FK_ID){
entityB_ID = entB_ID;
entityA_FK_ID = entA_FK_ID;
}
}
Note: I'm using Ebean 4.1.4 with Java 1.6.
So, this code works well, but there is a perk, which I'm trying to solve - the resultant table in the database looks like this:
entityB_ID [primary]
entityA_FK_ID [primary]
entityA_fk
As you can see that last column is redundant considering the "entityA_FK_ID" column.
What I would like to have?
I would like to be able to tell Ebean to use the column "entityA_FK_ID" for the association #ManyToOne instead of creating it's own column.
To solve this problem we have to:
Map EntityB.entityA to the same column as EntityB.ID.entityA_FK_ID
set 'insertable' and 'updateable' attributes of EntityB.entityA #JoinColumn annotation
Override setter of EntityB.entityA
Here is the code:
EntityA.java:
#Entity
public class EntityA extends Model {
#Id
private String ID;
#OneToMany(mappedBy="entityA")
public List<EntityB> listEntitesB;
public static Finder<String,EntityA> find = new Finder<String,EntityA>(
String.class, EntityA.class
);
public EntityA() {
ID = UUID.randomUUID().toString();
}
public String getID() {
return ID;
}
}
EntityB.java:
#Entity
public class EntityB extends Model {
#EmbeddedId
private EntityB_PK ID;
#ManyToOne
#JoinColumn(name = "entityA_fk_id", insertable = false, updatable = false)
private EntityA entityA;
public EntityA getEntityA() {
return entityA;
}
public void setEntityA(EntityA aEntityA) {
entityA = aEntityA;
ID.entityA_FK_ID = aEntityA.getID();
}
public EntityB(String entityB_ID){
ID = new EntityB_PK(UUID.randomUUID().toString(), entityB_ID);
}
public String getID() {
return ID.entityB_ID;
}
}
EntityB_PK.java:
#Embeddable
public class EntityB_PK implements Serializable {
public String entityB_ID;
#Column(name="entityA_fk_id")
public String entityA_FK_ID;
public EntityB_PK(String entB_ID, String entA_FK_ID){
entityB_ID = entB_ID;
entityA_FK_ID = entA_FK_ID;
}
#Override
public int hashCode() {
return entityB_ID.length() + entityA_FK_ID.length();
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
EntityB_PK b = (EntityB_PK)obj;
if(b==null)
return false;
if (b.entityB_ID.equals(entityB_ID) && b.entityA_FK_ID.equals(entityA_FK_ID)) {
return true;
}
return false;
}
}