JPA Specification multiple join based on foreignkey - mysql

I have following relationships between three objects
public class ProductEntity {
#Id
private int id;
#OneToMany(mappedBy = "productEntity",
fetch = FetchType.LAZY)
private List<ProductInfoEntity> productInfoEntityList = new ArrayList<>();
#Column(name = "snippet")
private String snippet;
}
public class ProductInfoEntity {
#Id
private int id;
#ManyToOne
#JoinColumn(name = "product_id")
private ProductEntity productEntity;
#ManyToOne
#JoinColumn(name = "support_language_id")
private SupportLanguageEntity supportLanguageEntity;
}
public class SupportLanguageEntity {
#Id
private int id;
#Column("name")
private String name;
}
And this is actual database design
Then, I'd like to make a specification to query as followings:
select * from product_info
where product_id = 1
and support_language_id = 2;
I am also using annotation for the specification which means that I use ProductEntity_, ProductInfoEntity_ and so on.
Can you please give me a full working code for the specification for query mentioned above?
Thank you guys

To use Specification your ProductInfoEntityRepository have to extend JpaSpecificationExecutor
#Repository
public interface ProductInfoEntityRepository
extends JpaRepository<ProductInfoEntity, Integer>, JpaSpecificationExecutor<ProductInfoEntity> {
}
As far as I understand you use JPA metamodel. So then
#Autowired
ProductInfoEntityRepository repository;
public List<ProductInfoEntity> findProductInfoEntities(int productId, int languageId) {
return repository.findAll((root, query, builder) -> {
Predicate productPredicate = builder.equal(
root.get(ProductInfoEntity_.productEntity).get(ProductEntity_.id), // or root.get("productEntity").get("id")
productId);
Predicate languagePredicate = builder.equal(
root.get(ProductInfoEntity_.supportLanguageEntity).get(SupportLanguageEntity_.id), // or root.get("supportLanguageEntity").get("id")
languageId);
return builder.and(productPredicate, languagePredicate);
});
}
If you want to make specifications reusable you should create utility class contains two static methods productIdEquals(int) and languageIdEquals(int).
To combine them use Specifications(Spring Data JPA 1.*) or Specification(since Spring Data JPA 2.0)

select * from product_info where product_id = 1 and support_language_id = 2;
Should work as written. But the only thing useful will be comment.
Perhaps you want the rest of the info in all three tables?
SELECT pi.comment, -- list the columns you need
p.snippet,
sl.name
FROM product AS p -- table, plus convenient "alias"
JOIN product_info AS pi -- another table
ON p.id = pi.product_info -- explain how the tables are related
JOIN support_language AS sl -- and another
ON pi.support_language_id = sl.id -- how related
WHERE p.snippet = 'abc' -- it is more likely that you will start here
-- The query will figure out the rest.
From there, see if you can work out the obfuscation provided by JPA.

Related

Include additional columns in Where clause of Hibernate/JPA Generated UPDATE Query

I am using Hibernate/JPA.
When i do an entity.save() or session.update(entity), hibernate generates a query like this :-
update TABLE1 set COL_1=? , COL_2=? , COL_3=? where COL_PK=?
Can I include an additional column in the WHERE clause by means of any annotation in the entity, so it can result in a query like :-
update TABLE1 set COL_1=? , COL_2=? , COL_3=? where COL_PK=? **AND COL_3=?**
This is because our DB is sharded based on COL_3 and this needs to be present in where clause
I want to be able to achieve this using the session.update(entity) or entity.save() only.
If I understand things correctly, essentially what you are describing is that you want hibernate to act like you have a composite primary key even though your database has a single-column primary key (where you also have a #Version column to perform optimistic locking).
Strictly speaking, there is no need for your hibernate model to match your db-schema exactly. You can define the entity to have a composite primary key, ensuring that all updates occur based on the combination of the two values. The drawback here is that your load operations are slightly more complicated.
Consider the following entity:
#Entity
#Table(name="test_entity", uniqueConstraints = { #UniqueConstraint(columnNames = {"id"}) })
public class TestEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id", nullable = false, unique = true)
private Long id;
#Id
#Column(name = "col_3", nullable = false)
private String col_3;
#Column(name = "value", nullable = true)
private String value;
#Version
#Column(nullable = false)
private Integer version;
... getters & setters
}
Then you can have the following method (in my case, I created a simple JUnit test)
#Test
public void test() {
TestEntity test = new TestEntity();
test.setCol_3("col_3_value");
test.setValue("first-value");
session.persist(test);
long id = test.getId();
session.flush();
session.clear();
TestEntity loadedTest = (TestEntity) session
.createCriteria(TestEntity.class)
.add(Restrictions.eq("id", id))
.uniqueResult();
loadedTest.setValue("new-value");
session.saveOrUpdate(loadedTest);
session.flush();
}
This generates the following SQL statements (enabled Hibernate logging)
Hibernate:
call next value for hibernate_sequence
Hibernate:
insert
into
test_entity
(value, version, id, col_3)
values
(?, ?, ?, ?)
Hibernate:
select
this_.id as id1_402_0_,
this_.col_3 as col_2_402_0_,
this_.value as value3_402_0_,
this_.version as version4_402_0_
from
test_entity this_
where
this_.id=?
Hibernate:
update
test_entity
set
value=?,
version=?
where
id=?
and col_3=?
and version=?
This makes loading slightly more complicated as you can see - I used a criteria here, but it satisfies your criteria, that your update statements always include the column col_3 in the 'where' clause.
The following solution works, however I recommend you to just wrap your saveOrUpdate method in a way that you ends up using a more natural approach. Mine is fine... but is a bit hacky.
Solution:
You can create your own annotation and inject your extra condition to hibernate save method using a hibernate interceptor. The steps are the following:
1. Create a class level annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
public #interface ForcedCondition {
String columnName() default "";
String attributeName() default ""; // <-- this one is just in case your DB column differs from your attribute's name
}
2. Annotate your entity specifying your column DB name and your entity attribute name
#ForcedCondition(columnName = "col_3", attributeName= "col_3")
#Entity
#Table(name="test_entity")
public class TestEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id", nullable = false, unique = true)
private Long id;
#Column(name = "col_3", nullable = false)
private String col_3;
public String getCol_3() {
return col_3;
}
... getters & setters
}
3. Add a Hibernate interceptor and inject the extra condition:
public class ForcedConditionInterceptor extends EmptyInterceptor {
private boolean forceCondition = false;
private String columnName;
private String attributeValue;
#Override
public boolean onSave(
Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
// If your annotation is present, backup attribute name and value
if (entity.getClass().isAnnotationPresent(ForcedCondition.class)) {
// Turn on the flag, so later you'll inject the condition
forceCondition = true;
// Extract the values from the annotation
columnName = entity.getClass().getAnnotation(ForcedCondition.class)).columnName();
String attributeName = entity.getClass().getAnnotation(ForcedCondition.class)).attributeName();
// Use Reflection to get the value
// org.apache.commons.beanutils.PropertyUtils
attributeValue = PropertyUtils.getProperty(entity, attributeName);
}
return super.onSave(entity, id, state, propertyNames, types);
}
#Override
public String onPrepareStatement(String sql) {
if (forceCondition) {
// inject your extra condition, for better performance try java.util.regex.Pattern
sql = sql.replace(" where ", " where " + columnName + " = '" + attributeValue.replaceAll("'", "''") + "' AND ");
}
return super.onPrepareStatement(sql);
}
}
After all that everytime you call entity.save() or session.update(entity) over an entity annotated with #ForcedCondition, the SQL will be injected with the extra condition you want.
BTW: I didn't tested this code but it should get you along the way. If I did any mistake please tell me so I can correct.

MySql insert data in wrong order from JPA

I am trying to import the data from excel ile to different tables in my DB.
The import is done , the only problem is that some of the data is in the wrong order.
This is what my excel file loks like.
And this is what the data in my Bd lokks like.
My service file has this method:
#Override
public List<Task> getTasksFromExcel(MultipartFile files) throws IOException {
List<Task> taskList = new ArrayList<>();
XSSFWorkbook workbook = new XSSFWorkbook(files.getInputStream());
XSSFSheet worksheet = workbook.getSheetAt(0);
String a = "A";
for (int index = 0; index <= worksheet.getPhysicalNumberOfRows(); index++) {
if (index > 1) {
Task task = new Task();
Lot lot = new Lot();
String ref = a + index;
CellReference cr = new CellReference(ref);
XSSFRow row = worksheet.getRow(cr.getRow());
String lotName = row.getCell(0).getStringCellValue();
Lot existingLot = lotRepository.findByName(lotName);
if (existingLot == null) {
lot.setName(lotName);
lotRepository.save(lot);
} else {
lot = existingLot;
}
;
task.setName(row.getCell(1).getStringCellValue());
String email = row.getCell(2).getStringCellValue();
Collaborator collab = collaboratorRepository.findByEmail(email);
task.setCollaborator(collab);
List<Double> iC = new ArrayList<>();
for (int i = 3; i < 6; i++) {
iC.add((Double) row.getCell(i).getNumericCellValue());
}
Set<Double> charge = new HashSet<Double>();
charge.addAll(iC);
task.setInitialCharge(charge);
task.setLot(lot);
taskList.add(task);
taskRepository.save(task);
}
}
return taskList;
}
And for the entity definition I go tthis.
#Entity
#Table(name = "task")
public class Task {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String description;
private Date assignment;
private Date deadline;
#ElementCollection
private Set<Double> initialCharge=new HashSet<Double>();
#Column(columnDefinition = "varchar(32)")
#Enumerated(EnumType.STRING)
private Status status = Status.TODO;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "lot_id")
private Lot lot;
#ManyToOne()
#JoinColumn(name = "collaborator_id")
private Collaborator collaborator;
You mapped this differently then your excel spreadsheet, and so lose the S1,S2,S3 column name/ordering you had for the single Task row, and seemed to have assumed that the set positional would be consistent, giving you S1-S3 for free. It does not.
Normalizing this out to allow expanding lists of charges and still having an order would mean adding a positional column to the task_initial_charges table. JPA will populate this column behind the scenes if you simply annotate your element collection with the OrderColumn to specify it:
The task_initial_charge needs s1,s2,s3 columns so that a single task_id has 3 positional columns, or you need another column in there to allow writing out the position within your initialCharge Set.
#ElementCollection
#OrderColumn
private Set<Double> initialCharge=new HashSet<Double>();
The order of the initialCharge set when the entity is persisted will then be stored in the database, and should be used when fetching the entity.

Hibernate deletion issue with a bidirectional association

I am using Spring Data JPA (1.7.2-RELEASE) in combination with Hibernate (4.3.8.Final) and MySQL (5.5). I want to manage two entities in a bidirectional assosciation. The save and update of the enties works fine, but the deletion doesn't work.
#Entity
public class Beacon extends AbstractEntity {
#OneToMany(fetch = FetchType.EAGER, mappedBy = "beacon", cascade = ALL)
private Set<Comment> comments;
/**
* #return the comments
*/
public Set<Comment> getComments() {
return comments;
}
/**
* #param comments the comments to set
*/
public void setComments(Set<Comment> comments) {
this.comments = comments;
}
}
and
#Entity
public class Comment extends AbstractEntity {
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "beacon_id")
private Beacon beacon;
public Beacon getBeacon() {
return beacon;
}
public void setBeacon(Beacon beacon) {
this.beacon = beacon;
}
}
Having a beacon with comments stored in the database, I want to delete the comment but it doesn't work. I don't get an exception but the entity is still present in the database.
This is my unit test:
#Test
public void deleteWithStrategyCheck() {
Beacon beacon = this.beaconRepository.save(createBeacon());
Comment comment = this.commentRepository.save(createEntity());
comment.setBeacon(beacon);
comment = this.commentRepository.save(comment);
this.commentRepository.delete(comment.getId());
assertThat(this.commentRepository.exists(comment.getId())).isFalse();
assertThat(this.beaconRepository.exists(beacon.getId())).isTrue();
assertThat(this.beaconRepository.findOne(beacon.getId()).getComments()).doesNotContain(comment);
}
If I delete the comment via a sql statement it works.
You need to add orphanRemoval = true to your #OneToMany mappings, and remove the Comment from the parrent beacon.
If you delete the Comment without removing it from the parrent collection you should actually get the exception (unless you are not using InnoDB storage engine, (and ou should)).
beacon.getComments().remove(comment),
will do the work then. (with orphanRemoval you don't need to call EM.remove(comment). Without it, you need to remove the comment from the collection and call EM.remove(comment).

OneToMany & ManyToOne in Hibernate Mapping

public class ClassYear {
private Set<Examination> examination = new HashSet<Examination>();
#Id
#Column(name = "class_id")
#GeneratedValue(generator = "system-uuid")
#GenericGenerator(name = "system-uuid", strategy = "uuid")
#DocumentId
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
#OneToMany(cascade={CascadeType.ALL},fetch=FetchType.EAGER)
#JoinColumn(name="class_id")
public Set<Examination> getExamination() {
return examination;
}
public void setExamination(Set<Examination> examination) {
this.examination = examination;
}
}
public class Examination {
private ClassYear classYear;
#ManyToOne(cascade={CascadeType.ALL},fetch=FetchType.EAGER)
#JoinColumn(name="class_id")
public ClassYear getClassYear() {
return classYear;
}
public void setClassYear(ClassYear classYear) {
this.classYear = classYear;
}
}
So here, Examination table refers the class_id column of ClassYear table. Now I need the class_id column to be used on few other tables. How should I do it..? It's One to many relationship concept but I need that class_id to be used on many tables (say like HomeWork table). I searched the Internet and found explanations for one to many & many to one concepts, but I can't find exactly how to do it for multiple tables with a particular column in a table as One to Many relationship.
The Examination entity owns the relationship, so you should map the examinations in ClassYear entity (which is the non-owning side of the relationship) using mappedBy like this (without join column):
#OneToMany(mappedBy="classYear", cascade={CascadeType.ALL})
public Set<Examination> getExamination() {
return examination;
}
I removed the EAGER fetch type in this example (the default for #OneToMany is LAZY). I don't know your use case but always eagerly fetching mapped collections like this is not the best idea.
If you need to reference ClassYear in other entities like HomeWork, do it the same way. The HomeWork entity will have #ManyToOne mapping to ClassYear and will be the owning side of the relationship (just like in Examination) and all the homeworks for the ClassYear can be mapped using mappedBy in the ClassYear entity.
Also one small detail:
#ManyToOne(cascade={CascadeType.ALL},fetch=FetchType.EAGER)
#ManyToOne has the EAGER fetch type as default, so you don't have to specify it manually.

The most efficient way to store photo reference in a database

I'm currently looking to store approximately 3.5 million photo's from approximately 100/200k users. I'm only using a mysql database on aws. My question is in regards to the most efficient way to store the photo reference. I'm only aware of two ways and I'm looking for an expert opinion.
Choice A
A user table with a photo_url column, in that column I would build a comma separated list of photo's that both maintain the name and sort order. The business logic would handle extracting the path from the photo name and append photo size. The downside is the processing expense.
Database example
"0ea102, e435b9, etc"
Business logic would build the following urls from photo name
/0e/a1/02.jpg
/0e/a1/02_thumb.jpg
/e4/35/b9.jpg
/e4/35/b9_thumb.jpg
Choice B - Relational Table joined on user table with the following fields. I'm just concerned I may have potential database performance issues.
pk
user_id
photo_url_800
photo_url_150
photo_url_45
order
Does anybody have any suggestions on the better solution?
The best and most common answer would be: choice B - Relational Table joined on user table with the following fields.
id
order
user_id
desc
photo_url_800
photo_url_150
photo_url_45
date_uploaded
Or a hybrid, wherein, you store the file names individually and add the photo directory with your business logic layer.
My analysis, your first option is a bad practice. Comma separated fields are not advisable for database. It would be difficult for you to update these fields and add description on it.
Regarding the table optimization, you might want to see these articles:
Optimizing MyISAM Queries
Optimizing InnoDB Queries
Here is an example of my final solution using the hibernate ORM, Christian Mark, and my hybrid solution.
#Entity
public class Photo extends StatefulEntity {
private static final String FILE_EXTENSION_JPEG = ".jpg";
private static final String ROOT_PHOTO_URL = "/photo/";
private static final String PHOTO_SIZE_800 = "_800";
private static final String PHOTO_SIZE_150 = "_150";
private static final String PHOTO_SIZE_100 = "_100";
private static final String PHOTO_SIZE_50 = "_50";
#ManyToOne
#JoinColumn(name = "profile_id", nullable = false)
private Profile profile;
//Example "a1d2b0" which will later get parsed into "/photo/a1/d2/b0_size.jpg"
//using the generatePhotoUrl business logic below.
#Column(nullable = false, length = 6)
private String fileName;
private boolean temp;
#Column(nullable = false)
private int orderBy;
#Temporal(TemporalType.TIMESTAMP)
private Date dateUploaded;
public Profile getProfile() {
return profile;
}
public void setProfile(Profile profile) {
this.profile = profile;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public Date getDateUploaded() {
return dateUploaded;
}
public void setDateUploaded(Date dateUploaded) {
this.dateUploaded = dateUploaded;
}
public boolean isTemp() {
return temp;
}
public void setTemp(boolean temp) {
this.temp = temp;
}
public int getOrderBy() {
return orderBy;
}
public void setOrderBy(int orderBy) {
this.orderBy = orderBy;
}
public String getPhotoSize800() {
return generatePhotoURL(PHOTO_SIZE_800);
}
public String getPhotoSize150() {
return generatePhotoURL(PHOTO_SIZE_150);
}
public String getPhotoSize100() {
return generatePhotoURL(PHOTO_SIZE_100);
}
public String getPhotoSize50() {
return generatePhotoURL(PHOTO_SIZE_50);
}
private String generatePhotoURL(String photoSize) {
String firstDir = getFileName().substring(0, 2);
String secondDir = getFileName().substring(2, 4);
String photoName = getFileName().substring(4, 6);
StringBuilder sb = new StringBuilder();
sb.append(ROOT_PHOTO_URL);
sb.append("/");
sb.append(firstDir);
sb.append("/");
sb.append(secondDir);
sb.append("/");
sb.append(photoName);
sb.append(photoSize);
sb.append(FILE_EXTENSION_JPEG);
return sb.toString();
}
}