MyBatis ... Get Last insert ID in loop "foreach" - mybatis

Thank you for help :)
I tried to get last id, and read many post about it, but i don't arrive to apply it in my case.
First Class
private Date date;
private List<AdsEntity> adsDetails;
... getters and setters
Second Class (AdsEntity)
private int id;
private String description;
There is the code where i try to get the last id :
Mapper
#Insert({
"<script>",
"INSERT INTO tb_ads_details (idMyInfo, adDate)"
+ " VALUES"
+ " <foreach item='adsDetails' index='index' collection='adsDetails' separator=',' statement='SELECT LAST_INSERT_ID()' keyProperty='id' order='AFTER' resultType='java.lang.Integer'>"
+ " (#{adsDetails.description, jdbcType=INTEGER}) "
+ " </foreach> ",
"</script>"})
void saveAdsDetails(#Param("adsDetails") List<AdsDetailsEntity> adsDetails);
In debugging mode, when I watch List I see the id still at 0 and don't get any id.
So what I wrote didn't workout :(
Solution Tried with the answer from #Roman Konoval :
#Roman Konoval
I apply what you said, and the table is fully well set :)
Just one problem still, the ID is not fulfill
#Insert("INSERT INTO tb_ads_details SET `idMyInfo` = #{adsDetail.idMyInfo, jdbcType=INTEGER}, `adDate` = #{adsDetail.adDate, jdbcType=DATE}")
#SelectKey(statement = "SELECT LAST_INSERT_ID()", before = false, keyColumn = "id", keyProperty = "id", resultType = Integer.class )
void saveAdsDetails(#Param("adsDetail") AdsDetailsEntity adsDetail);
default void saveManyAdsDetails(#Param("adsDetails") List<AdsDetailsEntity> adsDetails)
{
for(AdsDetailsEntity adsDetail:adsDetails) {
saveAdsDetails(adsDetail);
}
}
Thank for your help :)
Solution add to #Roman Konoval proposal from #Chris advice
#Chris and #Roman Konoval
#Insert("INSERT INTO tb_ads_details SET `idMyInfo` = #{adsDetail.idMyInfo, jdbcType=INTEGER}, `adDate` = #{adsDetail.adDate, jdbcType=DATE}")
#SelectKey(statement = "SELECT LAST_INSERT_ID()", before = false, keyColumn = "id", keyProperty = "adsDetail.id", resultType = int.class )
void saveAdsDetails(#Param("adsDetail") AdsDetailsEntity adsDetail);
default void saveManyAdsDetails(#Param("adsDetails") List<AdsDetailsEntity> adsDetails)
{
for(AdsDetailsEntity adsDetail:adsDetails) {
saveAdsDetails(adsDetail);
}
}
Thanks to all of you, for the 3 suggestions!!!

yes. it doesnt work.
please take a look at mapper.dtd
foreach-tag doesnt support/provide the following properties statement, keyProperty order and resultType
if you need the id for each inserted item please let your DataAccessObject handle iteration and use something like this in your MapperInterface
#Insert("INSERT INTO tb_ads_details (idMyInfo, adDate) (#{adsDetail.idMyInfo, jdbcType=INTEGER}, #{adsDetail.adDate, jdbcType=DATE})")
#SelectKey(before = false, keyColumn = "ID", keyProperty = "id", resultType = Integer.class, statement = { "SELECT LAST_INSERT_ID()" } )
void saveAdsDetails(#Param("adsDetail") AdsDetailsEntity adsDetail);
please ensure AdsDetailsEntity-Class provides the properties idMyInfoand adDate
Edit 2019-08-21 07:25
some explanation
referring to the mentioned dtd the <selectKey>-tag is only allowed as direct child of <insert> and <update>. it refers to a single Object that is passed into the mapper-method and declared as parameterType.
its only executed once and its order property tells myBatis wether to execute it before or after the insert/update statement.
in your case, the <script> creates one single statement that is send to and handled by the database.
it is allowed to combine #Insert with <script> and <foreach> inside and #SelectKey. but myBatis doesnt intercept/observe/watch database handling the given statement. and as mentioned before, #SelectKey gets executed only once, before or after #Insert-execution. so in your particular case #SelectKey returns the id of the very last inserted element. if your script inserts ten elements, only the new generated id of tenth element will be returned. but #SelectKey requires a class-property with getter and setter to put the selected id into - which List<?> doesnt provide.
example
lets say you want to save an Advertisement and its AdvertisementDetails
Advertisement has an id, a date and details
public class Advertisement {
private List<AdvertisementDetail> adDetails;
private Date date;
private int id;
public Advertisement() {
super();
}
// getters and setters
}
AdvertisementDetail has its own id, a description and an id the Advertisementit belongs to
public class AdvertisementDetail {
private String description;
private int id;
private int idAdvertisement;
public AdvertisementDetail() {
super();
}
// getters and setters
}
the MyBatis-mapper could look like this. #Param is not used, so the properties are accessed direct.
#Mapper
public interface AdvertisementMapper {
#Insert("INSERT INTO tb_ads (date) (#{date, jdbcType=DATE})")
#SelectKey(
before = false,
keyColumn = "ID",
keyProperty = "id",
resultType = Integer.class,
statement = { "SELECT LAST_INSERT_ID()" })
void insertAdvertisement(
Advertisement ad);
#Insert("INSERT INTO tb_ads_details (idAdvertisement, description) (#{idAdvertisement, jdbcType=INTEGER}, #{description, jdbcType=VARCHAR})")
#SelectKey(
before = false,
keyColumn = "ID",
keyProperty = "id",
resultType = Integer.class,
statement = { "SELECT LAST_INSERT_ID()" })
void insertAdvertisementDetail(
AdvertisementDetail adDetail);
}
the DataAccessObject (DAO) could look like this
#Component
public class DAOAdvertisement {
#Autowired
private SqlSessionFactory sqlSessionFactory;
public DAOAdvertisement() {
super();
}
public void save(
final Advertisement advertisement) {
try (SqlSession session = this.sqlSessionFactory.openSession(false)) {
final AdvertisementMapper mapper = session.getMapper(AdvertisementMapper.class);
// insert the advertisement (if you have to)
// its new generated id is received via #SelectKey
mapper.insertAdvertisement(advertisement);
for (final AdvertisementDetail adDetail : advertisement.getAdDetails()) {
// set new generated advertisement-id
adDetail.setIdAdvertisement(advertisement.getId());
// insert adDetail
// its new generated id is received via #SelectKey
mapper.insertAdvertisementDetail(adDetail);
}
session.commit();
} catch (final PersistenceException e) {
e.printStackTrace();
}
}
}

What Chris wrote about inability to get ids in the foreach is correct. However there is a way to implement id fetching in mapper without the need to do it externally. This may be helpful if you use say spring and don't have a separate DAO layer and your mybatis mappers are the Repository.
You can use default interface method (see another tutorial about them) to insert the list of items by invoking a mapper method for single item insert and single item insert method does the id selection itself:
interface ItemMapper {
#Insert({"insert into myitem (item_column1, item_column2, ...)"})
#SelectKey(before = false, keyColumn = "ID",
keyProperty = "id", resultType = Integer.class,
statement = { "SELECT LAST_INSERT_ID()" } )
void saveItem(#Param("item") Item item);
default void saveItems(#Param("items") List<Item> items) {
for(Item item:items) {
saveItem(item);
}
}

MyBatis can assign generated keys to the list parameter if your DB/driver supports multiple generated keys via java.sql.Statement#getGeneratedKeys() (MS SQL Server, for example, does not support it, ATM).
The following example is tested with MySQL 5.7.27 + Connector/J 8.0.17 (you should include version info in the question).
Be sure to use the latest version of MyBatis (=3.5.2) as there have been several spec changes and bug fixes recently.
Table definition:
CREATE TABLE le tb_ads_details (
id INT PRIMARY KEY AUTO_INCREMENT,
description VARCHAR(32)
)
POJO:
private class AdsDetailsEntity {
private int id;
private String description;
// getters/setters
}
Mapper method:
#Insert({
"<script>",
"INSERT INTO tb_ads_details (description) VALUES",
"<foreach item='detail' collection='adsDetails' separator=','>",
" (#{detail.description})",
"</foreach>",
"</script>"
})
#Options(useGeneratedKeys = true, keyProperty="adsDetails.id", keyColumn="id")
void saveAdsDetails(#Param("adsDetails") List<AdsDetailsEntity> adsDetails);
Note: You should use batch insert (with ExecutorType.BATCH) instead of multi-row insert (=<foreach/>) when inserting a lot of rows.

Related

How to get the id value from an EclipseLink ClassDescriptor?

We currently have the following, working soft delete customizer in place:
public class SoftDeleteCustomizer implements DescriptorCustomizer {
#Override
public void customize(ClassDescriptor descriptor) {
descriptor.getQueryManager().setDeleteSQLString(
String.format("UPDATE %s SET record_status = 'D', record_status_time = CURRENT_TIMESTAMP WHERE id = #ID",
descriptor.getTableName()
)
);
}
}
We now want to add the user that deleted the record. I could sanitize the username, but I would prefer to use a parameter / argument.
I rewrote the customizer and did not set an argument for the #ID, as it was already injected correctly somewhere. I then found out that it was not injected when you are using a DeleteObjectQuery (with arguments?). So I have to add an argument for the #ID it seems, but I don't know how to get the id / primary key value of the record / entity to be deleted from a ClassDescriptor.
This is what I have so far:
#Override
public void customize(final ClassDescriptor descriptor) {
final DeleteObjectQuery query = new DeleteObjectQuery();
query.addArgument("DELETED_BY", String.class);
query.addArgument("ID", Long.class);
query.addArgumentValue(SecurityUtils.getUsername());
query.addArgumentValue(...); // How to get the ID of the record to delete?
query.setSQLString(String.format(DELETE_SQL, descriptor.getTableName()));
descriptor.getQueryManager().setDeleteQuery(query);
}
Okay, as a workaround I used our audit listener which we added as one of the EntityListeners. It implements SessionCustomizer. There I was able to do:
#Override
public void postDelete(final DescriptorEvent event) {
final Long id = ((AbstractEntity) event.getObject()).getId();
// Create and execute update query to set the username
}

How to optimize SQL query in Anylogic

I am generating Agents with parameter values coming from SQL table in Anylogic. when agent is generated at source I am doing a v look up in table and extracting corresponding values from table. For now it is working perfectly but it is slowing down the performance.
Structure of Table looks like this
I am querying the data from this table with below code
double value_1 = (selectFrom(account_details)
.where(account_details.act_code.eq(z))
.list(account_details.avg_value)).get(0);
double value_min = (selectFrom(account_details)
.where(account_details.act_code.eq(z))
.list(account_details.min_value)).get(0);
double value_max = (selectFrom(account_details)
.where(account_details.act_code.eq(z))
.list(account_details.max_value)).get(0);
// Fetch the cluster number from account table
int cluster_num = (selectFrom(account_details)
.where(account_details.act_code.eq(z))
.list(account_details.cluster)).get(0);
int act_no = (selectFrom(account_details)
.where(account_details.act_code.eq(z))
.list(account_details.actno)).get(0);
String pay_term = (selectFrom(account_details)
.where(account_details.act_code.eq(z))
.list(account_details.pay_term)).get(0);
String pay_term_prob = (selectFrom(account_details)
.where(account_details.act_code.eq(z))
.list(account_details.pay_term_prob)).get(0);
But this is very slow and wants to improve the performance. someone mentioned that we can create a Java class and then add the table into collection . Is there any example where I can refer. I am finding it difficult to put entire code.
I have created a class using below code:
public class Customer {
private String act_code;
private int actno;
private double avg_value;
private String pay_term;
private String pay_term_prob;
private int cluster;
private double min_value;
private double max_value;
public String getact_code() {
return act_code;
}
public void setact_code(String act_code) {
this.act_code = act_code;
}
public int getactno() {
return actno;
}
public void setactno(int actno) {
this.actno = actno;
}
public double getavg_value() {
return avg_value;
}
public void setavg_value(double avg_value) {
this.avg_value = avg_value;
}
public String getpay_term() {
return pay_term;
}
public void setpay_term(String pay_term) {
this.pay_term = pay_term;
}
public String getpay_term_prob() {
return pay_term_prob;
}
public void setpay_term_prob(String pay_term_prob) {
this.pay_term_prob = pay_term_prob;
}
public int cluster() {
return cluster;
}
public void setcluster(int cluster) {
this.cluster = cluster;
}
public double getmin_value() {
return min_value;
}
public void setmin_value(double min_value) {
this.min_value = min_value;
}
public double getmax_value() {
return max_value;
}
public void setmax_value(double max_value) {
this.max_value = max_value;
}
}
Created collection object like this:
Pls provide an reference to add this database table into collection as a next step. then I want to query the collection based on the condition
You are on the right track here!
Every time you access the database to read data there is a computational overhead. So the best option is to access the database only once, at the start of the model. Create all the objects you need, store other data you will need later into Java classes, and then use the Java classes.
My suggestion is to create a Java class for each row in your table, like you have done. And then create a map object - like you have done, but with the key as String and the value as this new object.
Then on model start you can populate this map as follows:
List<Tuple> rows = selectFrom(customer).list();
for (Tuple row : rows) {
Customer customerData = new Customer(
row.get( customer.act_code ),
row.get( customer.actno ),
row.get( customer.avg_value )
);
mapOfCustomerData.put(customerData.act_code, customerData);
}
Where mapOfCustomerData is a linkedHashMap and customer is the name of the table
See the model created in this blog post for more details and an example on using a scenario object to store all the data from the Database in a separate object
Note: The code above is just an example - read this blog post for more details on using the AnyLogic INternal Database
Before using Java classes, try this first: click the "index" tickbox for all columns that you query with a WHERE clause.

Setting and getting a Global variable in Drools

I have something like this in drl file:
import java.lang.String
global String result;
rule ''Rule 1'' when some condition
then
result = "PASS";
kcontext.getKnowledgeRuntime().setGlobal("Result", result); // I got an "Unexpected global" exception.
System.out.println("result = "+ Result);
Also, I don't know how to access this global variable from my MyService.java class.
I was trying to set a global variable from the drl file not my java class like Service class.
All I had to do was the following and it worked successfully
import java.lang.String
global String result;
rule ''Rule 1''
when
some condition
then
String grade = "PASS";
kcontext.getKnowledgeRuntime().setGlobal("result", grade);
end
Also, the global variable name should match what I pass on the setGlobal("result",...).
And then get the global variable using the session I have in the Service class. like:
session.getGlobal("result");
Your rule should not be touching the 'kcontext'. What in the world are you trying to do? result = "PASS" is sufficient for setting the value of the global.
global String result
rule "Rule 1"
when
// some condition
then
result = "PASS";
end
Of course it's not going to work like you want it to because you need to change the value of the existing object; you can't overwrite it like that. Some options might be a "ResultsHolder" sort of class with a boolean variable you can set; or maybe even an AtomicBoolean that you can call set on.
To fire rules with a global, you need to add the global objects to the KieBase before invoking your rules:
var value = ...; // some OBJECT which you are going to pass in as a global
KieSession session = ruleBase.newStatefulSession();
session.insert(...); // insert data
session.setGlobal( "myGlobalFoo", value ); // sets the global; note the name must match the rule file!
session.fireAllRules();
After the rules are fired, you'll have your reference to value that you can use. This is also why you can't pass strings as globals and expect them to capture changes -- Java is pass-by-value, not pass-by-reference.
Here's an example for passing results out of the rules. This toy app will check the student's score on a test and then decide if they passed or failed.
Classes:
class Student {
private String name;
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
}
class Exam {
private String name;
private Double score;
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
public Double getScore() { return this.score; }
public void setScore(String score) { this.score = score; }
}
class ExamResults {
private List<String> results = new ArrayList<>();
public void logResults( String name, Double score, boolean passed ) {
this.results.add(name + " scored " + score + "%, which is a " + (passed ? "passing": "failing") + " grade.");
}
public List<String> getResults() { return this.results; }
}
Rule:
global ExamResults results;
rule "Evaluate exam"
when
Student( $name: name )
Exam ( $score: score, name == $name )
then
boolean passed = $score > 60.0;
results.logResults( $name, $score, passed );
end
Invocation:
List<Student> students = ...;
List<Exam> exams = ... ;
ExamResults results = new ExamResults();
KieSession session = ruleBase.newStatefulSession();
students.forEach( student -> session.insert(students) );
exams.forEach( exam -> session.insert(exam) );
session.setGlobal( "results", results);
session.fireAllRules();
// Print the results:
results.getResults().forEach(System.out::println);
If all you're trying to do is to get some data out of your rules (eg whether certain conditions match), I've written up answers about how to do that previously here and here. If you just want to know what rules triggered, you should write a listener which logs rule hits ("afterMatchFired").

JPA : Update operation without JPA query or entitymanager

I am learning JPA, I found out that we have some functions which is already present in Jparepository like save,saveAll,find, findAll etc. but there is nothing like update,
I come across one scenario where I need to update the table, if the value is already present otherwise I need to insert the record in table.
I created
#Repository
public interface ProductInfoRepository
extends JpaRepository<ProductInfoTable, String>
{
Optional<ProductInfoTable> findByProductName(String productname);
}
public class ProductServiceImpl
implements ProductService
{
#Autowired
private ProductInfoRepository productRepository;
#Override
public ResponseMessage saveProductDetail(ProductInfo productInfo)
{
Optional<ProductInfoTable> productInfoinTable =
productRepository.findByProductName(productInfo.getProductName());
ProductInfoTable productInfoDetail;
Integer quantity = productInfo.getQuantity();
if (productInfoinTable.isPresent())
{
quantity += productInfoinTable.get().getQuantity();
}
productInfoDetail =
new ProductInfoTable(productInfo.getProductName(), quantity + productInfo.getQuantity(),
productInfo.getImage());
productRepository.save(productInfoDetail);
return new ResponseMessage("product saved successfully");
}
}
as you can see, I can save the record if the record is new, but when I am trying to save the record which is already present in table it is giving me error related to primarykeyviolation which is obvious. I checked somewhat, we can do the update by creating the entitymanager object or jpa query but what if I dont want to use both of them. is there any other way we can do so ?
update I also added the instance of EntityManager and trying to merge the code
#Override
public ResponseMessage saveProductDetail(ProductInfo productInfo)
{
Optional<ProductInfoTable> productInfoinTable =
productRepository.findByProductName(productInfo.getProductName());
ProductInfoTable productInfoDetail;
Integer price = productInfo.getPrice();
if (productInfoinTable.isPresent())
{
price = productInfoinTable.get().getPrice();
}
productInfoDetail =
new ProductInfoTable(productInfo.getProductName(), price, productInfo.getImage());
em.merge(productInfoDetail);
return new ResponseMessage("product saved successfully");
but no error, no execution of update statements in log, any possible reasons for that ?
}
I suspect you need code like this to solve the problem
public ResponseMessage saveProductDetail(ProductInfo productInfo)
{
Optional<ProductInfoTable> productInfoinTable =
productRepository.findByProductName(productInfo.getProductName());
final ProductInfoTable productInfoDetail;
if (productInfoinTable.isPresent()) {
// to edit
productInfoDetail = productInfoinTable.get();
Integer quantity = productInfoDetail.getQuantity() + productInfo.getQuantity();
productInfoDetail.setQuantity(quantity);
} else {
// to create new
productInfoDetail = new ProductInfoTable(productInfo.getProductName(),
productInfo.getQuantity(), productInfo.getImage());
}
productRepository.save(productInfoDetail);
return new ResponseMessage("product saved successfully");
}

How do I execute "select distinct ename from emp" using GreenDao

How do I execute "select distinct ename from emp" using GreenDao
I am trying to get distinct values of a column of sqlite DB using GreenDao. How do I do it? Any help appreciated.
You have to use a raw query for example like this:
private static final String SQL_DISTINCT_ENAME = "SELECT DISTINCT "+EmpDao.Properties.EName.columnName+" FROM "+EmpDao.TABLENAME;
public static List<String> listEName(DaoSession session) {
ArrayList<String> result = new ArrayList<String>();
Cursor c = session.getDatabase().rawQuery(SQL_DISTINCT_ENAME, null);
try{
if (c.moveToFirst()) {
do {
result.add(c.getString(0));
} while (c.moveToNext());
}
} finally {
c.close();
}
return result;
}
Of course you can add some filter-criteria to the query as well.
The static String SQL_DISTINCT_ENAME is used for performance, so that the query string doesn't have to be built every time.
EmpDao.Properties and EmpDao.TABLENAME is used to always have the exact column-names and table-names as they are generated by greendao.