I'm trying to write a generic tool for copying Cassandra table contents from one keyspace to another one (probably in a different cluster). All the tables are not too large.
Here is what I do:
Session source = ...
Session destination = ...
TableMetadata table = ...
final ResultSet rs = source.execute("select * from " + table.getName());
String insertCql = ...
PreparedStatement preparedStatement = destination.prepare(insertCql);
for (Row row : rs) {
final BoundStatement boundStatement = preparedStatement.bind();
for (int i = 0; i < rs.getColumnDefinitions().size(); i++) {
// bind column value from row to bountStatement
}
session.execute(boundStatement);
}
The problem is how to copy the column value from row to boundStatement. I can read it with row.getObject(i), but there is no corresponding setObject() in BoundStatement.
More precisely, this method exists in version 2.2 of the driver (it's cassandra-driver-dse), but that version does not work with Cassandra 3, and in version 3 of the driver (cassandra-driver-core) setObject() method does not exist. Instead, there are bunch of set() methods, all of them require Class, TypeToken or TypeCodec.
Where can I get those? ColumnDefinition only gives me DataType. It seems a doubtful idea to use row.getObject(i).getClass() to get Class.
Maybe there is a better approach to this task (schema-agnostic copying)?
I can look at DataType if the column and make a case per type to use setString() and so on, but this seems a bit overcomplicated and fragile.
You need to use bind variables in your insert statement and then bind the prepared statement with the column values from the result. Something along the lines of:
String insertCql = "INSERT INTO ks.tb (...) values (?,?,...)";
for (Row row : rs) {
List bindVariables = new ArrayList();
for (int i = 0; i < rs.getColumnDefinitions().size(); i++) {
bindVariables.add(rs.getObject(i));
}
final BoundStatement boundStatement = preparedStatement.bind(bindVariables.toArray(new Object[0]));
session.execute(boundStatement);
}
Related
Am I missing something obvious here?
I have the following method to execute queries against an SQLite local database using org.xerial:sqlite-jdbc:3.21.0.
public List<HashMap<String, Object>> executeQuery(String sql, List<Object> vals) throws Exception {
List<HashMap<String, Object>> rows = new ArrayList<>();
Connection conn = getConnection();
try (PreparedStatement stmnt = conn.prepareStatement(sql)) {
if (!vals.isEmpty()) {
for (int i = 0; i < vals.size(); i++) {
stmnt.setObject(i + 1, vals.get(i));
}
}
ResultSet rs = stmnt.executeQuery();
ResultSetMetaData meta = rs.getMetaData();
HashMap<String, Object> row;
while (rs.next()) {
row = new HashMap<>();
for (int i = 0; i < meta.getColumnCount(); i++) {
row.put(meta.getColumnName(i + 1), rs.getObject(i + 1));
}
rows.add(row);
}
} finally {
putConnection(conn);
}
return rows;
}
However, when I pass the following SQL into the method along with the following values, they don't get set (but it also doesn't throw an exception). It's like it internally assigns it but forgets to tell the database.
SELECT * FROM 'airlines' WHERE 'name' LIKE ? LIMIT 1
vals: size = 1 {"MyAirline"}
I can see from debugging that it gets inside the loop to setObject.
In ANSI standard SQL, single quotes (') are used to delimit literal strings and double quotes (") are used to delimit table/column names. So
SELECT * FROM 'airlines' WHERE 'name' LIKE ? LIMIT 1
really means "select all columns from the literal string 'airlines' where the literal string 'name' matches the pattern supplied by the parameter".
Interestingly, SQLite seems to be clever enough to interpret the literal string 'airlines' as the table name "airlines" but it is still interpreting 'name' as a literal string. Therefore, for every row in "airlines" it is comparing the literal string 'name' to the string value 'MyAirline' and it never matches, so the ResultSet contains no rows.
Your SQL command text should be
SELECT * FROM "airlines" WHERE "name" LIKE ? LIMIT 1
so SQLite will compare the contents of the "name" column with the value 'MyAirline'.
I have following question.
I'm using JDBC in my project and I made simple method to insert data into my database.
My problem is: What to do when I want to insert something like sysdate or just NULL to auto increment? To my method I send only strings and writing NULL to string doesn't work.
Can you give me any advice how to improve it?
This is the code with constant null in query, but it isn't what I want to.
public static void insertInto(String Table, ArrayList<String> values) throws SQLException
{
Connection conn = JavaConnectDB.ConnectDb();
OraclePreparedStatement pst = null;
StringBuilder Query = new StringBuilder("INSERT INTO " + Table + " VALUES (NULL, ");
for (int i = 0; i < values.size(); i++)
{
Query.append("? ");
if (i + 1 != values.size())
Query.append(", ");
}
Query.append(")");
pst = (OraclePreparedStatement) conn.prepareStatement(Query.toString());
for (int i = 0; i < values.size(); i++)
{
pst.setString(i + 1, values.get(i));
}
pst.executeUpdate();
}
This method creates query like "INSERT INTO TABLE VALUES (NULL, ?, ? ,?)" and then fills gaps with values from array.
There is java.sql.PreparedStatement.setNull(int, int)
Try, e.g.
pst.setNull(1, Types.BIGINT);
for (int i = 1; i < values.size(); i++)
{
pst.setString(i + 1, values.get(i));
}
Change Types.BIGINT for apporpriate type for your column.
Note, that values.get(0) is just ignored, but should present in the array.
Problem is the setString function. If you have a string "NULL" or "SYSDATE", it will result in the query being quoted ('NULL', 'SYSDATE'), so this will be inserted as string.
According to this answer, pst.setString(n, null) should do the trick already, so inserting SQL NULL values is yet relatively easy, just insert a Java null value into the array where you want the database value to be SQL NULL.
SYSDATE gets more delicate. But I think, here comes something more fundamental into play: how are you going to handle data types other than VARCHAR (see setString documentation). If you really wanted such a generic method, I would rather to pass ArrayList<Object> as parameter (or with ellipsis ...) and call the appropriate setXXX method for the specific Object type - or setObject with appropriate Type parameter set. You could then create your own class SysDate which could easily be detected.
Is the database layout known to your application? Then I'd rather recommend to have a separate insert method for each table accepting exactly the number of required parameters of correct type, such as
bool insertIntoTest(int someValue, Integer anotherValue, String andAnotherOne)
{
Connection conn = JavaConnectDB.ConnectDb();
OraclePreparedStatement pst
= (OraclePreparedStatement) conn.prepareStatement(
"INSERT INTO TEST (someValue, anotherValue) VALUES(?, ?)"
);
pst.setInt(1, someValue);
if(anotherValue == 0)
pst.setNull(2, Types.INTEGER);
else
pst.setInt(2, anotherValue);
// can handle null already...
pst.setString(3, andAnotherOne);
/* ... (execute, try/catch, return) */
}
Well you should define exactly what do ou want to do.
It's not possible to put null in an autoinkrement field on database by definition
Auto-increment allows a unique number to be generated when a new record is inserted into a table.
So if you want just to insert some filed to your table and delegate the genration of autoincrement to your database, you should create your query like that :
INSERT INTO TABLE VALUES (?, ? ,?).
Example :
Table employee(id,time,name)
Query :
INSERT INTO employee (time, name) VALUES (?, ?)
Is there an efficient way to obtain a list (preferably an array, a ResultSet will do) to SELECT a lot of rows.
For example:
Connection con = DriverManager.getConnection(host,username,password);
String sql = "SELECT * FROM table_name WHERE food = ? AND expiration > ?";
PreparedStatement stmt = con.prepareStatement(sql);
Using the above code, I want to get all the food from a given array that isn't expired.
String[] foodList = {"banana","apple","orange",...}
where the expiration date is a constant date (lets say 3 days ago). However, the way I have it is that the String and PreparedStatement are in a for loop that loop the number of foods in the array to individually check the expiration date. This creates a lot of ResultSets after I execute each individually.
Most SQL Databases support a IN (list) expression. This is roughly equivalent to providing a or expression:
SELECT id FROM table WHERE food IN ('Apple', 'Banana') AND exp < ?
is similar to
SELECT id FROM table WHERE (food = 'Apple' or food = 'Banana') AND exp < ?
In both cases some RDBMS can optimize it.
However first of all there is a limitation in the number of list items you can specify in the IN or number of characters you can use in the statement. So if your list can be variable long you need to be prepared to run multiple statements.
Secondly you cannot* set a array as an argument to a PreparedStatement and expect it to work with IN.
Unfortunately in plain JDBC all you can do is to concatenate a String. This is frowned upon, but there is no good alternative (unless you want to do something like giving the list of foods as a single list and use a "instring" expression).
Make sure to add as many ? (but not too many) as you expect parameters and then set them in the IN:
String[] foods = ...;
int remain = foods.length;
int start = 0;
while(remain > 0)
{ if (remain >= 100)
executeBatch(foods, start, 100); start+=100; remain-=100;
else if (remain >= 30)
executeBatch(foods, start, 30); start+=30; remain-=30;
else {
executeBatch(foods, start, 1); start+=1; remain-=1;
}
}
void executeBatch(String[] f, int off, int len)
{
StringBuilder sqlBuf = StringBuilder("... IN(");
for(int i=0;i<len;i++) {
sqlBuf.append((i!=0)?",?":"?");
}
String sql = sqlBuf.append(") AND exp < ?").toString();
PreparedStatement ps = c.prepareStatement(sql);
for(int i=0;i<foods.length;i++)
ps.setString(i+1, foods[i+off]);
ps.setTimestamp(foods.length+1, now);
....
}
This avoids to generate a lot of different SQL statement to compile. (Only 100,30 or 1 ?)). You can use the same logic for the OR case.
* not to be confused with ARRAY database type.
Probably not the most elegant solution, and you won't get any performance benefit from the prepared statement (but you will get parameter binding):
StringBuilder sql = new StringBuilder("SELECT * FROM table_name WHERE expiration > ? AND food IN (");
for (int i = 0; i < foodList.length; i++) {
if (i > 0) {
sql.append(',');
}
sql.append('?');
}
sql.append(")");
Connection con = DriverManager.getConnection(host, username, password);
PreparedStatement stmt = con.prepareStatement(sql.toString());
stmt.setDate(1, expirationDate);
for (int i = 0; i < foodList.length; i++) {
stmt.setString(i + 2, foodList[i]);
}
ResultSet rs = stmt.executeQuery();
/* ... Do Stuff ... */
I have a query in which I am using Cursors in Select clause along with some other columns Values.I wanted to iterate through its result via ResultSet in Java.But couldn't find a way to get the Cusror from the result set.Is it possible to do so?Can anyone help me?
For eg
select name, roll_no,
cursor (select subj1
from Subject
where id = 'abc'
) as cusr1
from student
Here would be a generic way to read columns from a ResultSet.
while (resultSet.next()) { //Read every row
int columnCount = resultSet.getMetaData().getColumnCount();
for (int column = 1; column <= columnCount; column++) { //Read every column
String columnName = resultSet.getMetaData().getColumnName(column);
Object value = resultSet.getObject(columnName);
if (value != null) {
doSomething(columnName, value);
}
}
}
I assume you are using an Oracle DB.
I also assume Java 7 or higher (try with resources).
You are using a Cursor Expression in your SQL.
The Oracle JDBC Driver returns a java.sql.Resultset object when you call Resultset.getObject(column number|column label) for a Cursor Expression column.
So it is safe to cast:
try (Resultset innerResultset = (Resultset) outerResultset.getObject(column number/label)) {
while(innerResultset.next()) {
...
}
One comment: In many cases, for example when creating "master-detail" queries, you use a correlated Cursor Expression, meaning there is a join on a table that appears in the outer query.
I am trying to retrieve generated keys from an executeBatch() transaction but I only get the last key to be added.
this is my code:
PreparedStatement ps_insert = conn.prepareStatement(insertQuery, PreparedStatement.RETURN_GENERATED_KEYS);
for (int i = 0 ; i < adding_dates.length ; i++){
ps_insert.setInt(1, Integer.parseInt(consultant_id));
ps_insert.setDate(2, adding_dates[i]);
ps_insert.setInt(3, Integer.parseInt(room_id));
ps_insert.addBatch();
}
ps_insert.executeBatch();
ResultSet rs = ps_insert.getGeneratedKeys(); //<-- Only the last key retrieved
conn.commit();
What am I doing wrong?
EDIT: Apologies for not mentioning that I use H2 (http://www.h2database.com/html/main.html) database in embedded mode.
According to H2 jdbc driver javadocs, this is the normal behaviour:
Return a result set that contains the last generated auto-increment
key for this connection, if there was one. If no key was generated by
the last modification statement, then an empty result set is returned.
The returned result set only contains the data for the very last row.
You must iterate the ResultSet to retrieve the keys.
PreparedStatement ps_insert = conn.prepareStatement(insertQuery, PreparedStatement.RETURN_GENERATED_KEYS);
for (int i = 0 ; i < adding_dates.length ; i++){
ps_insert.setInt(1, Integer.parseInt(consultant_id));
ps_insert.setDate(2, adding_dates[i]);
ps_insert.setInt(3, Integer.parseInt(room_id));
ps_insert.addBatch();
}
ps_insert.executeBatch();
ResultSet rs = ps_insert.getGeneratedKeys(); //<-- Only the last key retrieved
if (rs.next()) {
ResultSetMetaData rsmd = rs.getMetaData();
int colCount = rsmd.getColumnCount();
do {
for (int i = 1; i <= colCount; i++) {
String key = rs.getString(i);
System.out.println("key " + i + "is " + key);
}
}
while (rs.next();)
}
conn.commit();
This is a limitation of H2 implementation. This is an issue.
For now use inserts/updates without batch, or query generated keys somehow through select.
If you are sharing a session/connection between 2 threads, and two of those threads try to execute statements at the same time, then you might see this kind of problem.
You probably need to either (a) use a connection pool or (b) synchronise your entire access to the DB.
for instance for option (b)
put a synchronize token infront of your method to make it thread safe
Just a thought as i dont know you complete execution context