How to use derived columns in Spring Data JPA associations - java

I have an entity TeamActivity:
#Entity
#Table(name = "teams_to_activities")
public class TeamActivity {
#Column(name = "scope_id", nullable = false)
private String scopeId;
#Column(name = "team_id", nullable = false)
private String teamId;
#Column(name = "activity_set_id", nullable = false)
private String activitySetId;
#Id
#Column(name = "scoped_team_activity_id", nullable = false)
private String scopedTeamActivityId;
}
And another entity ActivitySet:
#Entity
#Table(name = "activity_sets")
public class ActivitySet {
#Column(name = "scope_id", nullable = false)
private String scopeId;
#Column(name = "name", nullable = false)
private String name;
#Column(name = "description", nullable = false)
private String description;
#Id
#Column(name = "scoped_activity_set_id", nullable = false)
private String scopedActivitySetId;
}
There's no index on any other column besides the PK in both tables.
There's no FK constraint creating a relationship between these tables whatsoever. I have no idea why as this is a legacy system.
Technically, if I fetch a TeamActivity record, I can pick the scope_id and activity_set_id from it and combine them to form a scoped_activity_set_id which would be a valid PK to fetch the corresponding ActivitySet.
I know that TeamActivity -> ActivitySet is a N -> 1 association
I would like to leverage Spring Data JPA features to create an association from TeamActivity to ActivitySet such that when I fetch a TeamActivity from TeamActivityRepository, the corresponding ActivitySet is also returned.
I have created an association like this before using a combination of #JoinColumn and #MapsId but there was actually a single FK to use which is different here where source table has 2 columns I can combine to get the target's key.

If you are fully in control of the database, I may propose you create a Materialized View with the contents you desire from both tables and handle it as any other table with JPA, i.e, create #Entity model and CrudRepository<MVTeamActivitySet, String>.
If you are not fully in control of the database, one easy way to achieve it is to simply create a method that internally executes two lookup queries and retrieves the expected model you want. You will still be using using JPA correctly.
Querying two tables and joining desired fields in the code layer is quite common with denormalized DBs, sometimes you want to avoid the overhead of a Materialized View.
#Override
public TeamActivitySetDto findById(String scopedTeamActivityId) throws DemoCustomException {
Optional<TeamActivity> teamActivityEntity = teamActivityDao.getById(scopedTeamActivityId);
if(teamActivityEntity.isEmpty()) {
throw new DemoCustomException("teamActivity record not found");
}
String scopedActivitySetId =
teamActivityEntity.get().getScopeId() + ":" + teamActivityEntity.get().getActivitySetId();
Optional<ActivitySet> activitySetEntity = activitySetDao.getById(scopedActivitySetId);
if(activitySetEntity.isEmpty()) {
throw new DemoCustomException("activitySet record not found");
}
return TeamActivitySetDto.builder()
.description(activitySetEntity.get().getDescription())
.name(activitySetEntity.get().getName())
.scopedActivitySetId(activitySetEntity.get().getScopedActivitySetId())
.activitySetId(teamActivityEntity.get().getActivitySetId())
.scopedTeamActivityId(teamActivityEntity.get().getScopedTeamActivityId())
.scopeId(teamActivityEntity.get().getScopeId())
.teamId(teamActivityEntity.get().getTeamId())
.build();
}

Related

Spring JPA most efficient way to load ManyToOne relationship?

I have an entity called StoreQuantity, which stores the current in stock quantity of all products/items in a store:
#Data
#Entity
#Table(name = "STORE_QUANTITY")
public class StoreQuantity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "STORE_QUANTITY_ID", nullable = false)
private int storeQuantityId;
#ManyToOne
#JoinColumn(name = "PRODUCT_ID", nullable = false)
private Product product;
#Column(name = "INSTORE_QUANTITY")
private int instoreQuantity;
#JsonIgnore
#ManyToOne
#JoinColumn(name = "STORE_ID", nullable = false)
private Store store;
}
Corresponding Store entity:
#Entity
#Table(name = "store")
public class Store implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "STORE_ID", nullable = false)
private int storeId;
#Column(name = "NAME", nullable = false)
private String name;
#JsonIgnore
#OneToMany(mappedBy = "store")
private List<StoreQuantity> storeQuantityList;
}
Im trying to retrieve all the quantities of products in all stores, and export as csv. I currently have thought of two ways of doing so:
Either Retrieve the entire storequantity table in one call, and for each storequantity I print as csv.
public String currentStoreQuantitiesCSV() {
List<StoreQuantity> storeQuantityList = storeQuantityRepository.findAllByOrderByStoreDesc();
for (StoreQuantity storeQuantity : storeQuantityList) {
//StoreId
csvString.append(storeQuantity.getStore().getStoreId()).append(',');
//ProductId
csvString.append(storeQuantity.getProduct().getProductId());
//Product Quantity
csvString.append(storeQuantity.getInstoreQuantity());
csvString.append(',');
}
Or I call them by store:
public String currentStoreQuantitiesCSV() {
List<Store> storeList = storeRepository.findAll();
for (Store store:storeList){
List<StoreQuantity> storeQuantityList = store.getStoreQuantityList();
for (StoreQuantity storeQuantity : storeQuantityList) {
//Store Name
csvString.append(storeQuantity.getStore().getName()).append(',');
//ProductId
csvString.append(storeQuantity.getProduct().getProductId());
//Product Quantity
csvString.append(storeQuantity.getInstoreQuantity());
csvString.append(',');
}
}
They both work, now it's just a matter of efficiency and ram utilization. I read by default JPA will eagerly load any ManyToOne relationships: Default fetch type for one-to-one, many-to-one and one-to-many in Hibernate
So does this mean if I choose option 1, there will be as many copies of store objects for every storequantity object? This will be extremely bad as I only have 20-or so stores, but thousands and thousands of storequantities, and id each of them are loaded with their own store object it will be very bad. Or will every storequantity point to the same store Objects? I'm only considering method two because that way there wouldnt be a lot of store objects in memory.
I did some testing looking at the stack memory, it seems that JPA will automatically map all ManyToOne relationships to one object. So in this case for example we have one store, and 10 storequantities that have a ManyToOne to that store. JPA will only instantiate one store object and point all 10 storequantity objects to that one store, instead of creating one store for every storequantity object. So option 1 will be the most efficient as we decrease the amount of database calls.

How to Include non-matching id in JPA OneToOne mapping

So I have a table with a column that has non-foreign key (no actual table reference) reference on another table but the other table might not have a matching row
class Component {
#OneToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "PART_ID", referencedColumnName="PART_ID", nullable = true, insertable = false, updatable = false)
#NotFound(action = NotFoundAction.IGNORE)
private Part part
#Id
#Column(name = "COMPONENT_ID")
private Long id;
}
Part class
class Part {
#Id
#Column(name = "PART_ID")
private Long id;
private String name;
}
without this #NotFound(action = NotFoundAction.IGNORE) I am getting an error
but with this I am not getting an error and get null value but I need the id to be present
e.g) I am looking for this
{"component": {"id":12, "part":{"id":100,"name":null}}}
but I am getting this (if no match)
{"component": {"id":12, "part":null}}
but I am getting this (if match)
{"component": {"id":12, "part":{"id":100,"name":"part_name"}}}
Tried with nullable=false and some combinations for updatable and insertable and still nothing works
If table has reference on another table but the other table might not have a matching row , the database structure is probably broken.
Do you really need what you're asking for?
The database has 2 part_id. One in Component and the other in Part. Java code has only one - Part.
You can make an additional field
сlass Component {
private Long partId;
...
and in the #PostLoad method - create a new Part with the desired id.
#PostLoad
public void postLoad(){
if(part == null && partId!=null){
part= new Part();
part.setId(partId)
}
}
}

Hibernate OneToOne between PK's with lazy behaviour

I'm trying to achieve to have an entity called MyEntity along with another entity called MyEntityInfo using Hibernate 5.3.13.Final with annotations under Wildfly 18.
The idea is to have MyEntity store some commonly requested fields, and MyEntityInfo store some rarely requested fields. Both share the same primary key called SID (Long), and there is a FK from Info's SID to Entity's SID. There can be entities without info.
Normally you will not require the additional info. For example, I don't want the info entity to be fetched when I query my entity like this:
MyEntityImpl entity = em.find(MyEntityImpl.class, 1L);
However, when I run this code, I find that there's a second query, fetching the Info entity along the main one, as in an EAGER behaviour.
I'm mapping the relationship using #OneToOne. I've tried several combinations of FetchType, optional and #LazyToOne, but so far without success.
Here is the code for both MyEntity and MyEntityInfo classes (additional getters and setters removed):
MyEntity (ID generator is a custom sequence generator):
#Entity
#Table(name = MyEntityImpl.TABLE_NAME)
public class MyEntityImpl {
public static final String TABLE_NAME = "TMP_MY_ENTITY";
#Id
#GeneratedValue(strategy = GenerationType.TABLE, generator = "GEN_" +
TABLE_NAME)
#GenericGenerator(name = "GEN_" +
TABLE_NAME, strategy = CoreIdGenerator.ID_GENERATOR, parameters = {
#Parameter(name = "tableName", value = TABLE_NAME) })
#Column(name = "sid", nullable = false, unique = true)
private Long sid;
#OneToOne(mappedBy = "myEntity", cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true)
#LazyToOne(LazyToOneOption.NO_PROXY)
private MyEntityInfoImpl info;
#Column
private String field;
MyEntityInfo:
#Entity
#Table(name = MyEntityInfoImpl.TABLE_NAME)
public class MyEntityInfoImpl {
public static final String TABLE_NAME = "TMP_MY_ENTITY_INFO";
#Id
#Column(name = "SID", nullable = false, unique = true)
private Long sid;
#OneToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "SID", referencedColumnName = "SID", insertable = false, updatable = false, nullable = false)
private MyEntityImpl myEntity;
#Column(name = "INFO_FIELD")
private String infoField;
I've tried this solution, but as I said, it didn't work for me:
Hibernate lazy loading for reverse one to one workaround - how does this work?
I've managed to do something somewhat similar using #OneToMany and managing data manually, but that's not what I'd like to do. However, another alternatives and information on whether this can be achieved or not using #OneToOne, or the right design pattern to do this are also welcome.
PS: Database tables creation for SQL Server, in case you want to try it:
create table TMP_MY_ENTITY (SID NUMERIC(19,0) NOT NULL, FIELD VARCHAR(100));
go
ALTER TABLE TMP_MY_ENTITY ADD CONSTRAINT PK_TMP_MY_ENTITY PRIMARY KEY CLUSTERED (SID);
go
create table TMP_MY_ENTITY_INFO (SID NUMERIC(19,0) NOT NULL, INFO_FIELD VARCHAR(100));
go
ALTER TABLE TMP_MY_ENTITY_INFO ADD CONSTRAINT PK_TMP_MY_ENTITY_INFO PRIMARY KEY CLUSTERED (SID);
go
CREATE SEQUENCE SEQ_TMP_MY_ENTITY START WITH 1 INCREMENT BY 1 MINVALUE 1 CACHE 20;
alter table TMP_MY_ENTITY_INFO add constraint FK_TMP_MY_ENT_INFO_MY_ENT FOREIGN KEY (SID) references TMP_MY_ENTITY(SID);
go
insert into TMP_MY_ENTITY(SID, FIELD) VALUES (NEXT VALUE FOR SEQ_TMP_MY_ENTITY, 'Field 1');
insert into TMP_MY_ENTITY_INFO(SID, INFO_FIELD) VALUES ((SELECT MAX(SID) FROM TMP_MY_ENTITY), 'Info 1');
insert into TMP_MY_ENTITY(SID, FIELD) VALUES (NEXT VALUE FOR SEQ_TMP_MY_ENTITY, 'Field 2');
insert into TMP_MY_ENTITY_INFO(SID, INFO_FIELD) VALUES ((SELECT MAX(SID) FROM TMP_MY_ENTITY), 'Info 2');
insert into TMP_MY_ENTITY(SID, FIELD) VALUES (NEXT VALUE FOR SEQ_TMP_MY_ENTITY, 'Field 3 no info');
-- DELETE ALL
drop table TMP_MY_ENTITY_INFO;
drop table TMP_MY_ENTITY;
drop sequence SEQ_TMP_MY_ENTITY;
After following #SternK link, and upgrading to Wildfly 19 and Hibernate 5.4.14, it finally worked by using #MapsId.
The right mapping to use is this:
MyEntity:
public class MyEntityImpl {
#OneToOne(mappedBy = "myEntity", cascade = CascadeType.REMOVE, fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "SID")
private MyEntityInfoImpl info;
MyEntityInfo:
public class MyEntityInfoImpl {
#OneToOne(fetch = FetchType.EAGER, optional = false)
#MapsId
#JoinColumn(name = "SID", referencedColumnName = "SID", insertable = false, updatable = false, nullable = false)
private MyEntityImpl myEntity;

Conditional unique constraint using annotations

I have two related entities with two different types(GENERAL and CUSTOM) and I save it in the same table. Entity with type GENERAL should have unique values of field name and CUSTOM can have duplicates for different users and not duplicate GENERAL name.
I'm looking for a way to create conditional unique constraint in order to check next cases:
if entity has type GENERAL, name should be unique
if entity has type CUSTOM, name can be duplicated in the table but can't duplicate GENERAL items and should be unique for specific user(by user id)
#Entity
#Table(name = "Purpose", uniqueConstraints = #UniqueConstraint(columnNames = {"purposeId"}))
#Inheritance(strategy=InheritanceType.SINGLE_TABLE)
public class GeneralPurpose {
#Id
#GeneratedValue(strategy=GenerationType.AUTO, generator="purpose_seq_gen")
#SequenceGenerator(name="purpose_seq_gen", sequenceName="PURPOSE_SEQ")
#Column(name = "purposeId", nullable = false)
private long purposeId;
#Column(name = "type", nullable = false)
#Enumerated(EnumType.STRING)
private PurposeType type;
#Column(name = "name", nullable = false)
private String name;
#Entity
#Table(name = "Purpose")
public class CustomPurpose extends GeneralPurpose {
#ManyToOne()
#JoinColumn(name="id")
#JsonIgnore
private User user;
public enum PurposeType {
GENERAL, CUSTOM
}
You could do this by adding another column to the Purpose table. This column is to store a constant value for GENERAL records, and the user id for CUSTOM records. For GENERAL records, the value could be 0 (if the user id is numeric) or "GENERAL" (if the user id is a string). It could be named 'userOfRecord' or 'recordDiscriminator', something like that.
Then you can add a unique constraint on [ type, name, userOfRecord ].

How to delete Hibernate #Embeddables containing nullable fields?

When deleting an #Embeddable object, I run into some problems.
I have the following domain classes: SwitchVoipTrunkGroup and PrioritizedCodec. The latter contains several fields that are nullable.
class SwitchVoipTrunkGroup {
//...
#CollectionOfElements(fetch = FetchType.LAZY)
#JoinTable(
name = "SWITCH_VOIP_TKG_CODEC",
joinColumns = #JoinColumn(name = "FK_SWITCH_VOIP_TKG_ID")
)
#ForeignKey(name = "FK_CODEC_SWITCH_VOIP_TKG")
private Set<PrioritizedCodec> prioritizedCodecs = new HashSet<PrioritizedCodec>();
//...
}
#Embeddable
public class PrioritizedCodec {
#Column(name = "PRIORITY")
private String priority;
#Column(name = "FAX_MODE")
private String faxMode;
//... some more columns ...
}
When I edit SwitchVoipTrunkGroup's prioritizedCodecs field (e.g. by deleting an entry) and save the entity, I see the following in my Hibernate logging:
13:54:31,919 INFO [STDOUT] Hibernate: delete from T_SWITCH_VOIP_TKG_CODEC where
fk_switch_voip_tkg_id=? and fax_mode=? and priority=?
From this question I understand why Hibernate uses all the fields in the where clause. However, this gives problems: in case some of these fields are empty, the query will look like so:
delete from T_SWITCH_VOIP_TKG_CODEC where fk_switch_voip_tkg_id=1 and fax_mode = ''
and priority =''
This will however not delete any records, as what is really necessary is for Hibernate to check for NULL iso for an empty string. For example:
delete from T_SWITCH_VOIP_TKG_CODEC where fk_switch_voip_tkg_id=1 and fax_mode
IS NULL and priority IS NULL
(cf. here for more info on why checking for an empty string does not suffice)
Any ideas on how to tackle this? Many thx!
I suggest to normalize your database, so both your classes become entities, and then to setup One-to-Many relation between SwitchVoipTrunkGroup and PrioritizedCodec, then you may setup cascading rules so Hibernate automatically updates collection of elements of PrioritizedCodec type, when you persist instance of SwitchVoipTrungGroup.
#Entity
class SwitchVoipTrunkGroup {
//...
#OneToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST}, orphanRemoval = true)
#JoinColumn(name = "switchVoipTrunkGroup_id")
#ForeignKey(name = "FK_PrioritizedCodec_SwitchVoipTrunkGroup")
private Set<PrioritizedCodec> prioritizedCodecs = new HashSet<PrioritizedCodec>();
//...
}
#Entity
public class PrioritizedCodec {
#Column(name = "PRIORITY")
private String priority;
#Column(name = "FAX_MODE")
private String faxMode;
//... some more columns ...
}
#Serice("someService")
public class SomeService {
#Autowired
private SwitchVoipTrunkGroupDao trunkDao;
public SwitchVoipTrunkGroup doOperation("criteria") {
SwitchVoipTrunkGroup tg = trunkDao.find("criteroa");
tg.getPrioritizedCodecs().[remove(2)]; //remove should be implemened, that is just lame statement
tg.getPrioritizedCodecs().get(5).setFaxMod("ENABLED");
return trunkDao.save(tg); //hibernate will remove missing elements from PrioritizedCodec table, and will update necessary entities.
}
}
Alternatively, you may specify default values for priority and faxMode fields via attributes of #Column annotation and enforce nullable constraints
#Column(columnDefinition = "VARCHAR(20) default 'NONE'", nullable = false)
private String faxMode;

Categories

Resources