spring jdbc and composite primary keys - java

Is there a way in spring jdbc to return a composite primary key when a row is inserted.
This composite primary key is made up of values from separate sequences
Any help is greatly appreciated
Regards
Damien

Here is a full example (tested on PostgreSQL 8.4):
My table:
CREATE TABLE test
(
id serial NOT NULL,
otherid serial NOT NULL,
val text,
CONSTRAINT test_pkey PRIMARY KEY (id, otherid)
)
This is how you get keys back:
public void doStuff() {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(
new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
PreparedStatement ps = connection.prepareStatement("insert into test(val) values (?)", Statement.RETURN_GENERATED_KEYS);
ps.setInt(1, 42);
return ps;
}
},
keyHolder);
keyHolder.getKeys().get("id");
keyHolder.getKeys().get("otherid");
}
Now, if you want to get your composite key as an instance of some class directly from keyHolder, it is not simple.
JdbcTemplate uses ColumnMapRowMapper to map generated keys (generated keys are returned as result set, at least on PostgreSQL. It actually returns the whole row as if you were executing select on the row you just inserted). Same ColumnMapRowMapper is used in number of other places in JdbcTemplate.
The only possible point of extension here is KeyHolder itself. Here is what you can do:
public void doStuff() {
CompositeKeyHolder keyHolder = new CompositeKeyHolder();
... same code here ...
keyHolder.getCompositeKey();
}
class CompositeKeyHolder extends GeneratedKeyHolder {
private boolean converted;
public CompositeKey getCompositeKey() {
return new CompositeKey((Integer)this.getKeys().get("id"), (Integer)this.getKeys().get("otherid"));
}
}
class CompositeKey {
private Integer id;
private Integer otherId;
CompositeKey(Integer id, Integer otherId) {
this.id = id;
this.otherId = otherId;
}
public Integer getId() {
return id;
}
public Integer getOtherId() {
return otherId;
}
}

Here is the basic idea for a single key. The long id at the end is the key. If you have multiple sequences, I would recommend just using two separate statements to get each generated key.
JdbcTemplate template = getJdbcTemplate();
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(
new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
PreparedStatement ps = connection.prepareStatement(...);
return ps;
}
},
keyHolder);
long id = keyHolder.getKey().longValue();

What database server are you using? MySQL only allows one auto_increment field per table and I'd imagine this is often the case, but without knowing your setup it's hard to say. Assuming there is only one auto_generated field in your table, your INSERT would have had to be aware of the value going into the second PK field. Robert's code should work for retrieving the generated key value, and the cleanest solution would probably be to perform a SELECT after the fact using this generated key and the value which you had a hold of already.

I think what you need is GeneratedKeyHolder.getKeys(). Code would look like this example, except you will have to call
keyHolder.getKeys()
instead of
keyHolder.getKey()

Related

jdbc.update succed in database, but keyholder no content return

this is my code
private long saveTacoInfo(Taco taco) {
taco.setCreatedAt(new Date());
PreparedStatementCreator psc =
new PreparedStatementCreatorFactory(
"insert into Taco (name, createdAt) values (?, ?)",
Types.VARCHAR, Types.TIMESTAMP
).newPreparedStatementCreator(
Arrays.asList(
taco.getName(),
new Timestamp(taco.getCreatedAt().getTime())));
KeyHolder keyHolder = new GeneratedKeyHolder();
int res = jdbc.update(psc, keyHolder);
log.info("res " + res);
return keyHolder.getKey().longValue();
}
after debugging, found keyHolder.getKey() return null, because GeneratedKeyHolder keyList is empty!
any suggestions?
this is my Taco table sql:
create table if not exists Taco (
id identity,
name varchar(50) not null,
createdAt timestamp not null
);
the Insert statment is execute ok! h2 database has the data. but keyHolder.getKey is null.
I ran into the same problem as you. Apparently, you used the example from the book "Spring in action".
It seems that the problem in the code, namely in the "PreparedStatementCreatorFactory" class, the "returnGeneratedKeys" field is set to false.
I do not know why the example works in the book or why it is not specified that the variable needs to be changed. The value of this variable, according to Github, has not changed since 2008.
Here is a piece of class code:
public class PreparedStatementCreatorFactory {
/** The SQL, which won't change when the parameters change. */
private final String sql;
/** List of SqlParameter objects (may not be {#code null}). */
private final List<SqlParameter> declaredParameters;
private int resultSetType = ResultSet.TYPE_FORWARD_ONLY;
private boolean updatableResults = false;
private boolean returnGeneratedKeys = false;
#Nullable
private String[] generatedKeysColumnNames;
The problem is solved by setting the value of the "returnGeneratedKeys" field to true. Something like this:
var preparedStatementCreatorFactory = new PreparedStatementCreatorFactory(query, Types.VARCHAR, Types.TIMESTAMP);
preparedStatementCreatorFactory.setReturnGeneratedKeys(true);
PreparedStatementCreator preparedStatementCreator =
preparedStatementCreatorFactory
.newPreparedStatementCreator(Arrays.asList(users.getName(), users.getCreateDateTime()));

Unique constraint violated - shouldn't be possible, but still... why?

I do have a model class:
public class LimitsModel {
private Long id;
private Long userId;
private Long channel;
}
I also have a unique constraint on my entity set on fields userId and channel. Throughtout the application, there's no chance those could duplicate.
The limits were added mid development, so we already had users and channels and had to create limits entity for every existing user. So we're creating them during some operation and there's no other place they're created. Here's how we create them:
List<LimitsModel> limits = userDAO.getUserLimits(userId, channel);
if(isNull(limits) || limits.isEmpty()){
List<limitsModel> limitsToSave = this.prepareLimits();
limits = userDAO.createOrUpdateLimits(limitsToSave);
}
.
.
.
other operations
What I'm getting is
Caused by: java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (USER_LIMITS_UNIQUE) violated
Any clues what could be the case? I'm simply drawing the limits from the database, checking if they exist and if not creating them. Where's the place for unique constraint violation?
EDIT
createOrUpdateLimits just calls this method:
public void createOrUpdateAll(final Collection<?> objects) {
getHibernateTemplate().executeWithNativeSession(session -> {
Iterator<?> iterator = objects.iterator();
while (iterator.hasNext()) {
session.saveOrUpdate(iterator.next());
}
return null;
});
}
prepareLimits nothing complicated, a simple builder:
private List<LimitsModel> prepareLimits() {
List<LimitsModel> limitsToSave = LimitsModel.CHANNELS.stream()
.map(channel -> LimitsModel.builder()
.userId(UserUtils.getId())
.channel(channel)
.build())
.collect(Collectors.toList());
return scaLimitsToSave;
}
getUserLimits:
public List<LimitsModel> getUserLimits(Long userId, Long channel) {
return getHibernateTemplate().execute(session -> {
final Criteria criteria = session.createCriteria(LimitsModel.class)
.add(Restrictions.eq(LimitsModel.PROPERTY_USER_ID, userId));
if (nonNull(channel)){
criteria.add(Restrictions.eq(LimitsModel.PROPERTY_CHANNEL, channel));
}
return criteria.list();
});
}
The constraint is on userId, channel. There is a possibility that the block that gets the limits and then creates them is called twice. Shouldn't the new limits be already in the database when it's called the second time? Isn't the transaction commited already?

How to save a JavaBean contains list to Sql By using Mybatis?

As we all know, when using Hibernate, it can create sql tables for us. Even a Java Bean has a list, hibernate will create a foreign table for us.
However, when I use myBatis, I find it very inconvenient that I have to create the table by myself ahead. Then I can insert values to the table. What is more inconvenient and I am not sure is that when I have a JavaBean with a list, I want to save this document to mysql.
For example, My java bean:
public class Person {
public String id;
public List<String> interests;
}
Then, Mysql should have a primary table (person table) and a foreign table (interests table).
My question is: 1. Can mybatis create these two tables for me?
2. Can mybatis auto convert the javabean for me and insert values to both two tables. For instance (of course this is not correct)
<insert id="insertPerson">
INSERT INTO Person.java TO Database
</insert>
No, you do not want it. Do not use MyBatis in the same way as Spring Data or Hibernate.
About auto conversion. You should provide mapping for all related types (PersonInterest in our condition). Handle correct insertion on your Service layer. Selection could be provided by MyBatis with #Many(select = "selectPersonInterests")
#Mapper
public interface PersonMapper {
#InsertProvider(type = PersonProvider.class, method = "insertPerson")
void insertPerson(#Param("person") Person person);
#InsertProvider(type = PersonProvider.class, method = "insertPersonInterest")
void insertInterestItem(#Param("interest") Interest item);
}
#Component
public class PersonProvider {
public String insertPersonInterest() {
return "insert into person_interest (...) " +
"values (#{...}, ...);";
}
public String insertPerson() {
return "insert into person (...) " +
"values (#{...}, ...);";
}
}

Boolean value not getting mapped properly in Hibernate with MySQL

I am trying to map the result of an exists query (which returns TRUE/FALSE) from a MySQL database to a POJO via resultSetTransformer. I would hope the result of this exists query can get mapped to a boolean but it does not and throws the below error:
org.hibernate.PropertyAccessException: IllegalArgumentException
occurred while calling setter of TestBean.value
The cause of this exception is shown as:
java.lang.IllegalArgumentException: argument type mismatch
My sample class:
public class TestHibernate {
private static SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
public static void main(String[] args) throws ParseException {
try {
Query query = sessionFactory.openSession().createSQLQuery(
"SELECT " +
"EXISTS (SELECT * FROM A WHERE id = 3) AS value"
);
query.setResultTransformer(Transformers.aliasToBean(TestBean.class));
List<TestBean> beanList = (List<TestBean>) query.list();
} catch (Exception e) {
System.out.println(e);
}
}
}
The POJO:
public class TestBean {
private boolean value;
public boolean isValue() {
return value;
}
public void setValue(boolean value) {
this.value = value;
}
}
Am I missing something or is it a bug with Hibernate or MySQL JDBC Driver?
Hibernate version: 3.2.6GA
MySQL JDBC Driver: mysql-connector-java-5.1.2
Hibernate has a built-in "yes_no" type that would do what you want. It maps to a CHAR(1) column in the database.
Basic mapping:
<property name="some_flag" type="yes_no"/>
Annotation mapping (Hibernate extensions):
#Type(type="yes_no")
public boolean getFlag();
Your problem may be caused by the case mapping of the selected column, q.v. my solution below. I also parametrized your WHERE clause to avoid SQL injection.
Query query = session.createSQLQuery("select exists(select * from A where id = :id) as value");
.setParameter("id", "3");
.addScalar("value")
.setResultTransformer( Transformers.aliasToBean(TestBean.class))
List result = query.list();
TestBean theBean = (TestBean)result.get(0);
The transform of the result query can be explicitly set each parameter to the corresponding model datatype using hibernate addScalar() method. Please find the solution below.
Query query = sessionFactory.openSession().createSQLQuery(""+
"select " +
" exists(select * from A where id = 3) as value"
).addScalar("value", BooleanType.INSTANCE);
This will resolve to set to the Boolean value.
I know this is old answer, I tried to resolve this coz answer from here not worked for me.
with Addition to Answer from #anil bk, I overloaded a setter method accepting String as argument. Now It worked as expected.
public void setPriority(String priority) {
this.priority = "true".equals(priority);
}
Here is my answer

Mapping ResultSet to Pojo Objects

Well that's really embarrassing I have made a standard pojo class and its dao class for data retrieval purpose. I am having a difficulty to understand a basic procedure to how to handle a customized query data to Pojo class.
let's say my User class is
public class User{
private int userId;
private String username;
private int addressId;
}
public class Address{
private int addressId;
private String zip;
}
public class UserDAO{
public void getUserDetails(){
String getSql = select u.userId, u.username, a.zipcode from user u, address a where u.addressId = a.addressId;
//no pojo class is now specific to the resultset returned. so we can't map result to pojo object
}
}
now how I should model this with my pojo class as if using String to manage this then concept of object oriented vanishes, also complexity would increase in the future as well. kindly guide!
Update for Further Explanation
We know that we can map same table objects with same pojo class, but when the query is customized and there is a data returned which doesn't map to any specific class then what would be the procedure? i.e. should we make another class? or should we throw that data in a String variable? kindly give some example as well.
For this purpose you can use one of implementation of JPA. But as you want to do it manually I will give you small example.
UPD:
public class User {
private int userId;
private String username;
private Address address; // USE POJO not ID
}
public class Address{
private int addressId;
private String zip;
List<User> users;
}
public User getUserById(Connection con, long userId) {
PreparedStatement stmt;
String query = "select u.user_id, u.user_name, a.id, a.zip from user u, address a where a.address_id = u.id and u.id = ?";
User user = new User();
Address address = new Address;
try {
stmt = con.prepareStatement(query);
stmt.setLong(1, userId);
ResultSet rs = stmt.executeQuery();
address.setId(rs.getInt("id"));
address.setZip(rs.getString("zip");
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("user_name"));
user.setAddressId(rs.getInt("address_id"));
user.setAddress(address); // look here
} catch (SQLException e) {
if (con != null) {
try {
System.err.print("Transaction is being rolled back");
con.rollback();
} catch (SQLException excep) {
}
}
} finally {
if (stmt != null) {
stmt.close();
}
}
return user;
}
You shouldn't do new POJO for that query, you should write normal query. And remember - your object model is main, tables in DB is just a way to save data of your application.
We know that we can map same table objects with same pojo class, but when the query is customized and there is a data returned which doesn't map to any specific class then what would be the procedure? i.e. should we make another class?
JPA dynamic instantiation allows you to define a query with a POJO whose constructor specifies only the fields and types you want from the database.
This will perform a JPA selection which will return a List.
If you need to change the query later and the columns are unchanged, your POJO will still work.
If you change the columns, then also change the POJO accordingly.
NOTE:
You must specify fully qualified package and constructor arguments.
Type User must be a JPA-mapped or JPA-annotated entity class.
The entityManager is in JPA EntityManagerFactory.
TypedQuery<User> q;
String sql = "select new com.stuff.User(
int u.userId, String u.username, String a.zipcode)
from User u, Address a where u.addressId = a.addressId";
List<User> list = entityManager.createQuery(sql).getResultList();
for(User u : list) {
doStuff(u);
}
Dynamic instantiation is also handy when you want to select specified columns, but avoid those columns with large data, such as BLOB types.
For example, maybe you want a list of proxy POJO's which represent the fully populated thing, but are themselves not fully populated.
You present the proxy list, and when the user selects one, then you do another query to get the fully populated object.
Your mileage may vary.
There's many ORM frameworks that can do this including Hibernate, myBatis, JPA and spring-JDBC
spring-jdbc and myBatis give you granular control over the SQL whereas with JPA and Hibernate you are usually abstracted away from the SQL.
I suggest you do some reading and figure out which one you like before rolling your own solution.
Your question:
We know that we can map same table objects with same pojo class,
but when the query is customized and there is a data returned
which doesn't map to any specific class then what would be the procedure?
If you have 100 kinds of SQL which returns different combination of columns, could it be to create 100 different POJOs? The answer is "NO, stop using POJO".
This library qood is designed to solve this problem, you can try it.

Categories