View Javadoc

1   /* $Id: MysqlConnector.java,v 1.27 2006/01/12 15:13:08 psionides Exp $ */
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  		// TODO: dump other things like procedures, sequences, etc.
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 		/* the main map stores: foreign key name -> internal map
131 		   the internal map stores: sequence number in a foreign key -> name of the 
132 		       column in foreign key, name of the column in primary key, and
133 		       primary key table name */
134 		TreeMap<String, TreeMap<String, String[]>> fkeys
135 			= new TreeMap<String, TreeMap<String, String[]>>(); 
136 
137 		// read all the key data into the map
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 		// create proper CONSTRAINT commands from the key data
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 					// column 1 = name, column 2 = type
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 		// TODO: restore other things like procedures, sequences, etc.
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 					// ignore that - this means that table doesn't exist, which is not an error
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 			// prepare the query
300 			StringBuffer query = new StringBuffer("CREATE TABLE `" + table.getName() + "` (");
301 			
302 			Vector<Column> columns = table.getColumns();
303 	
304 			// get a list of constraints, but remove everything which is not a primary key
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 			// add all the columns
314 			for (int i=0; i<columns.size(); i++) {
315 				Column col = columns.get(i); 
316 				String type = col.getType();
317 				
318 				// type name may consist of many words, but it may be necessary 
319 				// to add a size parameter after the first word
320 				String[] typeWords = type.split(" ");
321 				type = typeWords[0].toUpperCase();
322 				
323 				// insert the first word
324 				query.append("`" + col.getName() + "` " + typeWords[0]);
325 				
326 				// for various number or character types, add size parameter 
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 				// add the rest of the words
335 				for (int j=1; j<typeWords.length; j++) {
336 					query.append(" " + typeWords[j]);
337 				}
338 				
339 				// add NULL/NOT NULL parameter
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 				// add DEFAULT parameter
348 				if (col.getDefault() != null) {
349 					query.append(" DEFAULT '" + col.getDefault().replaceAll("'", "//'") + "'");
350 				}
351 				
352 				// add other parameters
353 				if (col.getRemarks() != null && col.getRemarks().length() > 0) {
354 					query.append(" " + col.getRemarks());
355 				}
356 				
357 				// add a comma if it is not the end of the query
358 				if ((i != columns.size()-1) || constraints.size() > 0) {
359 					query.append(", ");
360 				}
361 			}
362 	
363 			// now add the primary key
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 			// finish building the query
374 			query.append(") ENGINE=InnoDB");
375 			
376 			// finally, execute the query
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 			// prepare the query
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 			// create a PreparedStatement to speed up the insert process
453 			PreparedStatement stat = connection.prepareStatement(query+"");
454 			
455 			// insert all records until the end of the table
456 			while (true) {
457 				try {
458 					String[] record = table.getDataLine();
459 					if (record == null) {
460 						// end of the table
461 						break;
462 					}
463 					
464 					// execute the PreparedStatement
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 		// the option at the end is needed to prevent empty date (0000-00-00) errors
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; // end of data
546 			}
547 		}
548 	}
549 }