How to handle unidirectional many-to-many relations with Ebean - many-to-many

I have a problem with Ebean. I have the usual Objects PsecUser, PsecRoles and PsecPermission.
A user can have many Permissions or Roles and a Role can have many Permission.
Here the code (extract):
#Entity
public class PsecPermission {
#Id
#GeneratedValue
private Long id;
#Column(unique=true, nullable=false)
private String name;
#Column(nullable=false)
private String type = PsecBasicPermission.class.getName();
#Column(nullable=false)
private String target;
#Column(nullable=false)
private String actions;
}
#Entity
public class PsecRole {
#Id
#GeneratedValue
private Long id;
#Column(unique=true, nullable=false)
private String name;
#Temporal(TemporalType.TIMESTAMP)
private Date lastUpdate;
#ManyToMany(fetch=FetchType.EAGER)
private List<PsecPermission> psecPermissions;
private boolean defaultRole = false;
}
I wrote the following helper-method:
public PsecRole createOrUpdateRole(String name, boolean defaultRole, String... permissions) {
PsecRole result = server.find(PsecRole.class).
where().eq("name", name).findUnique();
if (result == null) {
result = new PsecRole();
result.setName(name);
}
final List<PsecPermission> permissionObjects = server.find(PsecPermission.class).
where().in("name", (Object[])permissions).findList();
result.setPsecPermissions(permissionObjects);
result.setDefaultRole(defaultRole);
final Set <ConstraintViolation <PsecRole>> errors =
Validation.getValidator().validate(result);
if (errors.isEmpty()) {
server.save(result);
server.saveManyToManyAssociations(result, "psecPermissions");
} else {
log.error("Can't save role: " + name +"!");
for (ConstraintViolation <PsecRole> constraintViolation : errors) {
log.error(" " + constraintViolation);
}
}
return result;
}
and try the following test:
#Test
public void testCreateOrUpdateRole() {
String[] permNames = {"Test1", "Test2", "Test3"};
List <PsecPermission> permissions = new ArrayList <PsecPermission>();
for (int i = 0; i < permNames.length; i++) {
helper.createOrUpdatePermission(permNames[i], "target"+ i, "actions" +i);
PsecPermission perm = server.find(PsecPermission.class).where().eq("name", permNames[i]).findUnique();
assertThat(perm.getTarget()).isEqualTo("target" + i);
assertThat(perm.getActions()).isEqualTo("actions" + i);
permissions.add(perm);
}
PsecRole orgRole = helper.createOrUpdateRole(ROLE, false, permNames);
testRole(permNames, orgRole);
PsecRole role = server.find(PsecRole.class).where().eq("name", ROLE).findUnique();
testRole(permNames, role);
}
private void testRole(String[] permNames, PsecRole role) {
assertThat(role).isNotNull();
assertThat(role.getName()).isEqualTo(ROLE);
assertThat(role.isDefaultRole()).isEqualTo(false);
assertThat(role.getPermissions()).hasSize(permNames.length);
}
Which fails if it checks the number of permissions at the readed role. It's always 0.
I looked into the database and found that psec_role_psec_permission is alway empty.
Any idea what's wrong with the code?
You can get a pure Ebean-example from https://github.com/opensource21/ebean-samples/downloads it uses the eclipse-plugin from ebean.

There are two solutions for this problem:
Simply add cascade option at PsceRole
#ManyToMany(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
private List<PsecPermission> psecPermissions;
and remove server.saveManyToManyAssociations(result, "psecPermissions"); you find it in the cascade-solution-branch.
The cleaner solution, because you don't need to define cascase- perhaps you don't want it:
Just don't replace the list, just add your entries to the list. Better is to add new and remove old one. This mean in createOrUpdateRole:
result.getPsecPermissions().addAll(permissionObjects);
instead of
result.setPsecPermissions(permissionObjects);

Related

How To Insert Data with Spring Data JPA Relationships

hopefully this is an easy question, but I'm pretty new with JPA and having difficulty determining how to format a JSON POST request body that is sent to a Spring API. I have two entities, Product and Barcode with the following relationship:
One Product can have many barcodes that point to it (OneToMany)
One Barcode can point to only one Product (OneToOne)
The relationship is defined by a product_id column in the Barcode table.
Relationship definition in Barcode entity:
#OneToOne // one barcode relates to one product
private ProductEntity product;
The relationship definition in Product:
#OneToMany(cascade = CascadeType.ALL)
private List<BarcodeEntity> barcodes = new ArrayList<>();
My question is, how can I do a JSON POST request to insert a Barcode that is related to a Product already in the database? Would I need to pass the entire Product entity or is there a way to just pass the product_id alone?
How I would like to create a new Barcode entry when the Product already exists:
{
"barcode": "string",
"barcodeStatus": "string",
"codeStandard": "string",
"product": 1,
"title": "string",
"unitQuantity": 0
}
Instead of having to do the following, which I believe will result in an error because the product already exists:
{
"barcode": "string",
"barcodeStatus": "string",
"codeStandard": "string",
"product": {
productInfo: "...",
....,
},
"title": "string",
"unitQuantity": 0
}
Essentially, I'm trying to figure out how you can insert a new entity and define its relationship to another entity that is already present in the database. I'm sure I'm over complicating it.
Endpoint in Product controller:
#PostMapping(produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
public ProductRest createProduct(#Valid #RequestBody ProductRequestModel productDetails) throws Exception {
ProductRest returnValue = new ProductRest();
ModelMapper modelMapper = new ModelMapper();
ProductDto productDto = modelMapper.map(productDetails, ProductDto.class);
ProductDto createdProduct = productService.createProduct(productDto);
returnValue = modelMapper.map(createdProduct, ProductRest.class);
return returnValue;
}
createProduct method in Product service file:
public ProductDto createProduct(ProductDto product) {
if (productRepo.findByTitle(product.getTitle()) != null)
throw new ServiceException("Record with matching title already exists");
// Set product id for each barcode
for (int i = 0; i < product.getBarcodes().size(); i++) {
ProductBarcodeDto barcode = product.getBarcodes().get(i);
barcode.setProduct(product);
product.getBarcodes().set(i, barcode);
}
ModelMapper modelMapper = new ModelMapper();
ProductEntity productEntity = modelMapper.map(product, ProductEntity.class);
ProductEntity storedProductDetails = productRepo.save(productEntity);
ProductDto returnValue = modelMapper.map(storedProductDetails, ProductDto.class);
return returnValue;
}
Fields in ProductDto (defines getters/setters and empty constructor; just not shown):
public class ProductDto {
private long id;
private List<BarcodeDto> barcodes;
private String title;
private String description;
private String SKU;
private ProductVariationDto variation;
private double cost;
private double retailPrice;
private LocalDate launchDate;
private LocalDate discontinueDate;
private String discontinueReason;
private String salesChannel;
private LabelDto label;
private int secondaryStockLevel;
private int primaryStockLevel;
private LocalDateTime modifiedDate;
private LocalDateTime createdDate;
private String productStatus;
private List<SupplierDto> suppliers;
}
I had it set up so that ProductRequestModel expected a BarcodeEntity. Is this the correct or should I change it to expect just an integer value for the Barcode ID?
createBarcode endpoint in Barcode Controller:
#PostMapping(produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
public BarcodeRest createBarcode(#Valid #RequestBody BarcodeRequestModel barcodeDetails) throws Exception {
BarcodeRest returnValue = new BarcodeRest();
ModelMapper modelMapper = new ModelMapper();
BarcodeDto barcodeDto = modelMapper.map(barcodeDetails, BarcodeDto.class);
BarcodeDto createdBarcode = barcodeService.createBarcode(barcodeDto);
returnValue = modelMapper.map(createdBarcode, BarcodeRest.class);
return returnValue;
}
createBarcode method implementation:
public BarcodeDto createBarcode(BarcodeDto barcode) {
ModelMapper modelMapper = new ModelMapper();
if (barcodeRepo.findByBarcode(barcode.getBarcode()) != null)
throw new ServiceException("Barcode value already exists.");
BarcodeEntity barcodeEntity = modelMapper.map(barcode, BarcodeEntity.class);
BarcodeEntity storedBarcode = barcodeRepo.save(barcodeEntity);
BarcodeDto returnValue = modelMapper.map(storedBarcode, BarcodeDto.class);
return returnValue;
}
Firstly, there seems to be an issue with the Database Schema Mapping:
One Product : Many Barcodes (One to Many) [NO ISSUES]
Many Barcodes : One Product (Many to One, instead of One to One) [HAS ISSUES]
Secondly, yes you can use just the 'product_id' to update the barcode. It'd be better if you post the controller code once. Would be easier to find any issues with that.

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.

Room returns incorrect initialized object from generated query

I have three tables, one containing Cards, one containing CardDecks and third one implementing a many-to-many relation between the former two and additionally containg a symbol for every relation entry.
My task is to get three columns from the card-table and the symbol from the relation-table and save it in a data Object specifically designed for handling those inputs, the codition being, that all entries match the given deckId. Or in (hopefully correct) sql-language:
#Query("SELECT R.symbol, C.title, C.type, C.source " +
"FROM card_table C JOIN cards_to_card_deck R ON C.id = R.card_id"+
"WHERE R.card_deck_id = :cardDeckId")
LiveData<List<CardWithSymbol>> getCardsWithSymbolInCardDeckById(long cardDeckId);
But the room implementation class generates:
#Override
public LiveData<List<CardWithSymbol>> getCardsWithSymbolInCardDeckById(long
cardDeckId) {
final String _sql = "SELECT R.symbol, C.title, C.typ, C.source FROM
cards_to_card_deck R INNER JOIN card_table C ON R.card_id = C.id WHERE
R.card_deck_id = ?";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
int _argIndex = 1;
_statement.bindLong(_argIndex, cardDeckId);
return new ComputableLiveData<List<CardWithSymbol>>() {
private Observer _observer;
#Override
protected List<CardWithSymbol> compute() {
if (_observer == null) {
_observer = new Observer("cards_to_card_deck","card_table") {
#Override
public void onInvalidated(#NonNull Set<String> tables) {
invalidate();
}
};
__db.getInvalidationTracker().addWeakObserver(_observer);
}
final Cursor _cursor = __db.query(_statement);
try {
final int _cursorIndexOfSymbol = _cursor.getColumnIndexOrThrow("symbol");
final List<CardWithSymbol> _result = new ArrayList<CardWithSymbol>(_cursor.getCount());
while(_cursor.moveToNext()) {
final CardWithSymbol _item;
final int _tmpSymbol;
_tmpSymbol = _cursor.getInt(_cursorIndexOfSymbol);
_item = new CardWithSymbol(_tmpSymbol,null,null,null);
_result.add(_item);
}
return _result;
} finally {
_cursor.close();
}
}
#Override
protected void finalize() {
_statement.release();
}
}.getLiveData();
}
Where
_item = new CardWithSymbol(_tmpSymbol,null,null,null);
should return my fully initialized object.
The CardWithSymbol class is declared as follows:
public class CardWithSymbol {
public int symbol;
public String cardName;
public String cardType;
public String cardSource;
public CardWithSymbol(int symbol, String cardName, String cardType, String cardSource){
this.symbol = symbol;
this.cardName = cardName;
this.cardType = cardType;
this.cardSource = cardSource;
}
And the types of the columns returned by the query are:
int symbol, String title, String type, String source
I already went through some debugging and the rest of the application works just fine. I can even read the symbol from the objects return by the query, but as mentioned above for some reason room ignores the other three parameters and just defaults them to null in the query-implementation.
So after some trial and error and reading through the dao-documentation once again i found my error:
When creating a class for handling subsets of columns in room, it is important to tell room which variable coresponds to which columns via #ColumnInfo(name = "name of the column goes here")-annotation.
So changing my CardWithSymbol class as follows solved the issue for me:
import android.arch.persistence.room.ColumnInfo;
public class CardWithSymbol {
#ColumnInfo(name = "symbol")
public int symbol;
#ColumnInfo(name = "title")
public String cardName;
#ColumnInfo(name = "type")
public String cardType;
#ColumnInfo(name = "source")
public String cardSource;
public CardWithSymbol(int symbol, String cardName, String cardType, String cardSource){
this.symbol = symbol;
this.cardName = cardName;
this.cardType = cardType;
this.cardSource = cardSource;
}
}

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();
}
}

Jsp sql update query

<%
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/questionaire", "root", "root");
Statement st=con.createStatement();
ResultSet rs=st.executeQuery("Select * from question");
List arrlist = new ArrayList();
while(rs.next()){
String xa =rs.getString("display");
if(xa.equals("1")){
arrlist.add(rs.getString("q"));
}
}
Collections.shuffle(arrlist); %>
<%for(int i=0;i<5;i++){
String str = (String) arrlist.get(i);%>
<%=str%> //1
<%st.executeUpdate("update question set display='0' where id=?");%> //comment 2
<br>
<%}%>
This is my code.I have some questions which are displayed,then I shuffle them and randomly select 5 questions.The 5 randomly selected questions need to be given display='0' as can be seen in comment 2.How do I do it.I need to pass the id that str has to the database.Could anyone help?
1.) When selecting your questions you should not only "remember" the question-text but also the id. Why not create a new "Question"-class that can keep both values and possibly some more information(correct answer etc.).
public class Question {
private int id;
private String questionText;
private String answer;
private boolean display=false;
public Question(int id,String questionText,String answer) {
this.id = id;
this.questionText = questionText;
this.answer= answer;
}
public int getId() {
return id;
}
public String getQuestionText() {
return questionText;
}
public String getAnswer() {
return answer;
}
public boolean getDisplay() {
return display;
}
public void setDisplay(boolean display) {
this.display = display;
}
}
For each entry in your result-set create a new Question-object and add it to your question-list.
2.) You can't use the =?-syntax with a plain jdbc-Statement-object. You will have to use PreparedStatement for this, then you can set your query-parameters via the setXXX()-methods:
PreparedStatement stmt = conn.prepareStatement("update question set display='0' where id=?");
stmt.setInt(1, question.getId());
stmt.executeUpdate();
3.) When multiple users access your application I'm pretty sure you will get in trouble keeping your "display-state" in the database. Instead use the display-property of the Question-object (see above).