1
2 package net.sourceforge.jdbdump.connect.connectors;
3
4 import java.sql.DatabaseMetaData;
5 import java.sql.PreparedStatement;
6 import java.sql.ResultSet;
7 import java.sql.SQLException;
8 import java.sql.Statement;
9 import java.util.TreeMap;
10 import java.util.Vector;
11
12 import net.sourceforge.jdbdump.connect.DatabaseConnectionData;
13 import net.sourceforge.jdbdump.connect.DatabaseConnector;
14 import net.sourceforge.jdbdump.dump.Column;
15 import net.sourceforge.jdbdump.dump.Constraint;
16 import net.sourceforge.jdbdump.dump.Dump;
17 import net.sourceforge.jdbdump.dump.Table;
18
19 import org.apache.log4j.Logger;
20
21 /***
22 * This is a class handling connection to the database, an implementation of the abstract
23 * class DatabaseConnector. It is a specific connector, providing connection to MySQL databases.
24 *
25 * @see DatabaseConnector
26 * @author jsuder
27 */
28
29 public class MysqlConnector extends DatabaseConnector {
30
31 /*** A log4j logger for this class. */
32 private static Logger logger = Logger.getLogger(MysqlConnector.class);
33
34 /*** A ResultSet storing table data when it is being downloaded. */
35 protected ResultSet dataSet = null;
36
37 /*** The query used to download next parts of a table into dataSet. */
38 protected String query = null;
39
40 /*** Number of current record in a table while it is being downloaded. */
41 protected int position;
42
43 /*** Number of columns in the current table. */
44 protected int columns;
45
46 /*** Maximum number of table records downloaded at a time. */
47 public static final int FETCH_SIZE = 1000;
48
49 /***
50 * Opens a connection to the database, using the provided connection data.
51 * @param url a correct connection URL
52 * @param user user's login in the database
53 * @param pass user's password
54 * @throws SQLException if the connection is not possible
55 * @throws ClassNotFoundException if there is a problem with loading a driver
56 */
57
58 public void connect(String url, String user, String pass) throws SQLException, ClassNotFoundException {
59 connect(url, user, pass, "com.mysql.jdbc.Driver");
60 }
61
62 /***
63 * Creates a backup of the structure of the entire database. It doesn't backup
64 * any data.
65 *
66 * @todo dump other things like procedures, sequences, etc.
67 * @return a Dump object containing information about database structure
68 * @throws SQLException if the backup process is interrupted by a critical error
69 */
70
71 public Dump dump() {
72 Dump dump = new Dump();
73 dumpTables(dump);
74
75
76
77 dump.setDatabaseReader(this);
78 return dump;
79 }
80
81 /***
82 * Downloads the table's primary key from the database. This method implements
83 * the empty method dumpTablePrimaryKeys() in the superclass, so is will be called
84 * by superclass's dumpTable() after it retrieves all the columns.
85 *
86 * @param table a table which should be checked for primary keys
87 * @throws SQLException if the backup process is interrupted by a critical error
88 */
89
90 protected void dumpTablePrimaryKeys(Table table) throws SQLException {
91 ResultSet rsKeys = meta.getPrimaryKeys(null, null, table.getName());
92
93 if (!rsKeys.next()) {
94 return;
95 }
96
97 TreeMap<String, String> pkeys = new TreeMap<String, String>();
98
99 do {
100 pkeys.put(rsKeys.getString("KEY_SEQ"), rsKeys.getString("COLUMN_NAME"));
101 } while (rsKeys.next());
102
103 String conText = "PRIMARY KEY (";
104 boolean first = true;
105 for (String key : pkeys.keySet()) {
106 if (!first) {
107 conText = conText + ", ";
108 } else {
109 first = false;
110 }
111 conText = conText + "`" + pkeys.get(key) + "`";
112 }
113 conText = conText + ")";
114
115 table.addConstraint(new Constraint("PRIMARY", Constraint.ConstraintType.PRIMARY_KEY, conText));
116 }
117
118 /***
119 * Downloads the table's foreign (imported) keys from the database. This method implements
120 * the empty method dumpTableForeignKeys() in the superclass, so is will be called
121 * by superclass's dumpTable() after it retrieves all the columns.
122 *
123 * @param table a table which should be checked for foreign keys
124 * @throws SQLException if the backup process is interrupted by a critical error
125 */
126
127 protected void dumpTableForeignKeys(Table table) throws SQLException {
128 ResultSet rsKeys = meta.getImportedKeys(null, null, table.getName());
129
130
131
132
133
134 TreeMap<String, TreeMap<String, String[]>> fkeys
135 = new TreeMap<String, TreeMap<String, String[]>>();
136
137
138 while (rsKeys.next()) {
139 TreeMap<String, String[]> map = fkeys.get(rsKeys.getString("FK_NAME"));
140 if (map == null) {
141 map = new TreeMap<String, String[]>();
142 fkeys.put(rsKeys.getString("FK_NAME"), map);
143 }
144 String[] tab = new String[3];
145 tab[0] = rsKeys.getString("FKCOLUMN_NAME");
146 tab[1] = rsKeys.getString("PKTABLE_NAME");
147 tab[2] = rsKeys.getString("PKCOLUMN_NAME");
148 map.put(rsKeys.getString("KEY_SEQ"), tab);
149 }
150
151
152 for (String fkey : fkeys.keySet()) {
153 String fkname = fkey;
154 String pktable = "";
155
156 TreeMap<String, String[]> map = fkeys.get(fkey);
157 boolean first = true;
158 String fkcolumns = "", pkcolumns = "";
159 for (String keyseq : map.keySet()) {
160 String[] tab = map.get(keyseq);
161 pktable = tab[1];
162 if (!first) {
163 fkcolumns = fkcolumns + ", ";
164 pkcolumns = pkcolumns + ", ";
165 } else {
166 first = false;
167 }
168 fkcolumns = fkcolumns + "`" + tab[0] + "`";
169 pkcolumns = pkcolumns + "`" + tab[2] + "`";
170 }
171
172 String conText = "CONSTRAINT `" + fkname + "` FOREIGN KEY (" + fkcolumns
173 + ") REFERENCES `" + pktable + "` (" + pkcolumns + ")";
174 table.addConstraint(new Constraint(fkname,
175 Constraint.ConstraintType.FOREIGN_KEY, conText));
176 table.addConstraint(new Constraint(fkname,
177 Constraint.ConstraintType.OTHER, "KEY (" + fkcolumns + ")"));
178 }
179 }
180
181 /***
182 * Downloads the structure of a single table column from the database. This method
183 * overrides the method in superclass and will be called by superclass's dumpTable()
184 * for each column in a table. The specific behaviour which is needed for MySQL is
185 * handling the enum data type, because enum's possible options can't be retrieved
186 * directly through DatabaseMetaData.getColumns(), but only using a SHOW COLUMNS
187 * query.
188 *
189 * @param column a column which should have its parameters downloaded
190 * @param rsColumns a ResultSet returned by DatabaseMetaData.getColumns(),
191 * set on this column's record
192 * @throws SQLException if the backup process is interrupted by a critical error
193 */
194
195 protected void dumpColumn(Column column, ResultSet rsColumns) throws SQLException {
196 super.dumpColumn(column, rsColumns);
197 if (column.getType().equals("enum")) {
198 Statement st = connection.createStatement();
199 ResultSet rs = st.executeQuery("SHOW COLUMNS IN " + rsColumns.getString("TABLE_NAME"));
200 while (rs.next()) {
201 if (rs.getString(1).equals(column.getName())) {
202
203 column.setType(rs.getString(2));
204 return;
205 }
206 }
207 }
208 }
209
210 /***
211 * Restores the structure of the database from a Dump object loaded from a previously
212 * created backup file. It doesn't restore any data.
213 *
214 * @todo restore other things like procedures, sequences, etc.
215 * @param dump a Dump object containing information about database structure
216 * @throws SQLException if the restore process is interrupted by a critical error
217 */
218
219 public void restore(Dump dump) throws SQLException {
220 clearDatabase(dump);
221 for (Table table : dump.getTables()) {
222 restoreTableStructure(table);
223 restoreTableData(table);
224 }
225 for (Table table : dump.getTables()) {
226 restoreTableConstraints(table);
227 }
228
229
230 }
231
232 /***
233 * Clears tables from the database if they already exist, before they are
234 * inserted from a dump. This method is called by restore().
235 *
236 * @param dump the dump which contains names of the tables which should be cleared
237 * @throws SQLException if the clear process is interrupted by a critical error
238 */
239
240 protected void clearDatabase(Dump dump) throws SQLException {
241 try {
242 Statement st = connection.createStatement();
243 for (Table table : dump.getTables()) {
244 try {
245 clearConstraints(table);
246 } catch (SQLException e) {
247
248 logger.debug(e);
249 }
250 }
251 for (Table table : dump.getTables()) {
252 logger.debug("DROP TABLE IF EXISTS `" + table.getName() + "`");
253 st.executeUpdate("DROP TABLE IF EXISTS `" + table.getName() + "`");
254 }
255 } catch (SQLException e) {
256 logger.error("Couldn't clear database: " + e);
257 }
258 }
259
260 /***
261 * Removes constraints from the database's tables (if they exist), before they are
262 * dropped from the database and then inserted from a dump. This method is called by
263 * clearDatabase() and is needed because it may not be possible to drop some of the
264 * tables if they have constraints binding them to other tables.
265 *
266 * @param table the table which contains constraints which should be removed
267 * @throws SQLException if the clear process is interrupted by a critical error
268 */
269
270 protected void clearConstraints(Table table) throws SQLException {
271 Statement st = connection.createStatement();
272
273 String query = "ALTER TABLE `" + table.getName() + "` DROP FOREIGN KEY ";
274 Vector<Constraint> constraints = table.getConstraints();
275 for (int i=0; i<constraints.size(); i++) {
276 Constraint con = constraints.get(i);
277 if (con.getType() != Constraint.ConstraintType.FOREIGN_KEY) continue;
278
279 logger.debug(query + "`" + con.getName() + "`");
280 st.executeUpdate(query + "`" + con.getName() + "`");
281 }
282
283 st.close();
284 }
285
286 /***
287 * Creates a single table using information from a dump. It restores all the
288 * fields, also adds a primary key if there is one, but doesn't upload any data yet
289 * and doesn't add constraints other than the primary key.
290 *
291 * @param table the table which should be added to the database
292 * @throws SQLException if the create process is interrupted by a critical error
293 */
294
295 protected void restoreTableStructure(Table table) throws SQLException {
296 try {
297 Statement st = connection.createStatement();
298
299
300 StringBuffer query = new StringBuffer("CREATE TABLE `" + table.getName() + "` (");
301
302 Vector<Column> columns = table.getColumns();
303
304
305 Vector<Constraint> constraints = new Vector<Constraint>();
306 for (int i=0; i<table.getConstraints().size(); i++) {
307 Constraint con = table.getConstraints().get(i);
308 if (con.getType() == Constraint.ConstraintType.PRIMARY_KEY) {
309 constraints.add(con);
310 }
311 }
312
313
314 for (int i=0; i<columns.size(); i++) {
315 Column col = columns.get(i);
316 String type = col.getType();
317
318
319
320 String[] typeWords = type.split(" ");
321 type = typeWords[0].toUpperCase();
322
323
324 query.append("`" + col.getName() + "` " + typeWords[0]);
325
326
327 if (type.startsWith("INT") || type.endsWith("INT") || type.endsWith("CHAR")) {
328 query.append(" (" + col.getLength() + ")");
329 } else if (type.equals("REAL") || type.equals("DOUBLE") || type.equals("FLOAT")
330 || type.equals("DECIMAL") || type.equals("NUMERIC")) {
331 query.append(" (" + col.getLength() + ", " + col.getDecimalDigits() + ")");
332 }
333
334
335 for (int j=1; j<typeWords.length; j++) {
336 query.append(" " + typeWords[j]);
337 }
338
339
340 if (col.getNullable() == DatabaseMetaData.columnNullable) {
341 query.append(" NULL");
342 } else if ((col.getNullable() == DatabaseMetaData.columnNoNulls)
343 && (col.getNullable2().equals("NO"))) {
344 query.append(" NOT NULL");
345 }
346
347
348 if (col.getDefault() != null) {
349 query.append(" DEFAULT '" + col.getDefault().replaceAll("'", "//'") + "'");
350 }
351
352
353 if (col.getRemarks() != null && col.getRemarks().length() > 0) {
354 query.append(" " + col.getRemarks());
355 }
356
357
358 if ((i != columns.size()-1) || constraints.size() > 0) {
359 query.append(", ");
360 }
361 }
362
363
364 for (int i=0; i<constraints.size(); i++) {
365 Constraint con = constraints.get(i);
366 String text = con.getText();
367 query.append(text);
368 if (i != constraints.size()-1) {
369 query.append(", ");
370 }
371 }
372
373
374 query.append(") ENGINE=InnoDB");
375
376
377 logger.debug(query+"");
378 st.executeUpdate(query+"");
379 st.close();
380 } catch (SQLException e) {
381 logger.error("Couldn't restore the structure of table "
382 + table.getName() + ": " + e);
383 }
384 }
385
386 /***
387 * Restores the table's constraints (e.g. foreign keys). This method is called
388 * at the end of restoring process, after the data is uploaded, because otherwise
389 * we would have to find a correct order in which to download the data to prevent
390 * any conflicts.
391 *
392 * @param table the table whose constraints should be added
393 * @return SQLException if the constraints can't be added for some reason
394 */
395
396 protected void restoreTableConstraints(Table table) throws SQLException {
397 try {
398 Statement st = connection.createStatement();
399
400 String query = "ALTER TABLE `" + table.getName() + "` ADD ";
401 Vector<Constraint> constraints = table.getConstraints();
402 for (int i=0; i<constraints.size(); i++) {
403 Constraint con = constraints.get(i);
404 if (con.getType() == Constraint.ConstraintType.OTHER) {
405 String text = con.getText();
406 logger.debug(query + text);
407 st.executeUpdate(query + text);
408 }
409 }
410 for (int i=0; i<constraints.size(); i++) {
411 Constraint con = constraints.get(i);
412 if (con.getType() == Constraint.ConstraintType.FOREIGN_KEY) {
413 String text = con.getText();
414 logger.debug(query + text);
415 st.executeUpdate(query + text);
416 }
417 }
418
419 st.close();
420 } catch (SQLException e) {
421 logger.error("Couldn't restore constraints of table " + table.getName() + ": " + e);
422 }
423 }
424
425 /***
426 * Restores the table's data into the database. The data are loaded from
427 * the backup file record after record just before they are uploaded into
428 * the database.
429 *
430 * @param table the table whose data should be uploaded
431 * @throws SQLException when the data can't be uploaded
432 */
433
434 protected void restoreTableData(Table table) throws SQLException {
435 try {
436 table.initializeData();
437
438
439 StringBuffer query = new StringBuffer("INSERT INTO `" + table.getName() + "` VALUES (");
440 int columns = table.getColumns().size();
441 for (int i=0; i<columns; i++) {
442 Column col = table.getColumns().get(i);
443 if (i != 0) {
444 query.append(", ?");
445 } else {
446 query.append("?");
447 }
448 }
449 query.append(")");
450 logger.debug(query+"");
451
452
453 PreparedStatement stat = connection.prepareStatement(query+"");
454
455
456 while (true) {
457 try {
458 String[] record = table.getDataLine();
459 if (record == null) {
460
461 break;
462 }
463
464
465 for (int i=0; i<columns; i++) {
466 stat.setString(i+1, record[i]);
467 }
468 stat.executeUpdate();
469 } catch (Exception e) {
470 logger.error("Can't read record of data from the dump: " + e);
471 return;
472 }
473 }
474 } catch (SQLException e) {
475 logger.error("Couldn't restore data of table " + table.getName() + ": " + e);
476 }
477 }
478
479 /***
480 * Creates a MySQL-specific JDBC connection url (it has the form:
481 * jdbc:mysql://host:port/database).
482 * @param data an object providing all the data necessary to open the connection
483 * @return a connection url string generated from the data
484 */
485
486 public String createURL(DatabaseConnectionData data) {
487 String port = data.getPort();
488 String portStr;
489
490 try {
491 int portNr = Integer.parseInt(port);
492 portStr = ":" + port;
493 } catch (NumberFormatException e) {
494 portStr = "";
495 }
496
497
498 return "jdbc:mysql://" + data.getServer() + portStr + "/" + data.getDatabaseName()
499 + "?zeroDateTimeBehavior=convertToNull";
500 }
501
502 /***
503 * Prepares the given table for downloading its data. That means, it runs a query
504 * "SELECT * FROM `tablename`" and stores the received result set.
505 * @param tableName name of the table from which data should be downloaded
506 * @throw SQLException if the resultset can't be returned
507 */
508
509 public void initializeTableData(String tableName) throws SQLException {
510 Statement statement = connection.createStatement();
511
512 query = "SELECT * FROM `" + tableName + "`";
513 dataSet = statement.executeQuery(query + " LIMIT " + FETCH_SIZE);
514 columns = dataSet.getMetaData().getColumnCount();
515 position = FETCH_SIZE;
516 }
517
518 /***
519 * Downloads one record of data from the previously initialized table in database.
520 * @return next record from the table, or null if there are no more records
521 * @throws SQLException if data record can't be downloaded because of a connection error
522 * or an error in the database
523 */
524
525 public String[] getTableDataLine() throws SQLException {
526 if (dataSet.next()) {
527 String[] dataLine = new String[columns];
528 for (int i=0; i<columns; i++) {
529 dataLine[i] = dataSet.getString(i+1);
530 }
531 return dataLine;
532 } else {
533 Statement statement = connection.createStatement();
534 dataSet = statement.executeQuery(query + " LIMIT " + position + ", " + FETCH_SIZE);
535 position += FETCH_SIZE;
536
537 if (dataSet.next()) {
538 String[] dataLine = new String[columns];
539 for (int i=0; i<columns; i++) {
540 dataLine[i] = dataSet.getString(i+1);
541 }
542 return dataLine;
543 } else {
544 logger.info("getTableDataLine(): no more data lines in the table.");
545 return null;
546 }
547 }
548 }
549 }