Pull to refresh

UI для Firebird на Java

Reading time9 min
Views4.2K

Вступление


Год назад потребовалось написать БД в рамках курсовой работы. Особого труда это не вызвало. Выбрал тему, начертил ER-диаграмму, определился с полями таблиц и начал написание. Язык долго не выбирал, на тот момент начинал работать на Java в Eclipse. Выбрал СУБД, мой выбор пал на Firebird. Добавил таблиц через IBExpert и был всем доволен, как только написал UI для пары таблиц понял что можно создавать остальные с помощью копипаста. Код получился ужасный(ООП? не не слышал, так можно это было охарактеризовать), но на тот момент меня все радовало. Прошел год и по воле случая пришлось пересматривать свой код. Это было нечто страшное с непонятной структурой.

Перед собой решил поставить несколько целей:
— простое добавление таблиц
— применить, наконец, ООП
— применить шаблоны проектирования(для обучения)

Также сейчас непонятно почему людям в институте сложно писать простые БД (или лень), в любом случае, хочу показать простоту написания БД и познакомить со своим видением приложения (на мой взгляд очень простым).

Начало работы


Для написания БД нам потребуется
— Eclipse IDE for java developers
— Firebird
— Jaybird ( JDBC драйвер, по сути jar библиотека )
— IBExpert ( для добавления таблиц )

Все это можно просто скачать, поэтому вопрос о настройке environment'a пропущу. В этой статье реализую интерфейс для одной таблицы, т.к. остальные таблицы можно будет легко добавить.

Написание кода


У нас будет всего одна таблица Ranks с колонками ID и RANK.
image

Для написания интерфейса выбрал Swing.
Обязанности интерфейса будут такие
— Выбор таблицы
— Вывод\ Обновление таблицы
— Добавление\ Удаление\ Вставка записи
В итоге у нас будет вот такой интерфейс

Архитектуру приложения представляю следующим образом
— главный класс с main(..) (Application)
— соединение с нашей БД (DBHelper)
— общая модель для любой таблицы (AbsTable, Tables)
— базовый класс для всех таблиц(BaseFrame) и классы таблиц наследников (RankFrame)
— класс, создающий компоненты (Components)
— вспомогательный класс для строк, навеяно андроидом (Strings)
Написание модели таблицы

Тип наших данных, очевидно, ID — Integer и Rank — String. Названия колонок очевидны.
Заносим данные эти данные в наш класс Tables. Все вновь создаваемые таблицы тоже требуется описать здесь, по заданному шаблону.
public class Tables {
	public static final Class<?>[] RANKS_TYPE = { 
		Integer.class, 
		String.class 
	};
	public static final String[] RANKS_TABLE = { 
		"ID", 
		"Rank" 
	};
}

Также создаем класс AbsTable(наследованный от AbstractTableModel), который реализует модель для данных в таблице, нужно переопределить несколько методов базового класса. Реализация простая, принимает данные из класса Tables, для создания матрицы данных. Так можно модель для таблиц в общем виде и избежать бесполезного копирования кода для каждой таблицы.
public class AbsTable extends AbstractTableModel {
	private List<String> mColumnNames;
	private List<ArrayList<Object>> mTableData;
	private List<Object> mColumnTypes;

	public AbsTable(Class<?>[] types, String[] columns) {
		mColumnTypes = new ArrayList<Object>(types.length);
		mColumnNames = new ArrayList<String>(columns.length);
		for (int i = 0; i < columns.length; ++i) {
			mColumnTypes.add(i, types[i]);
			mColumnNames.add(columns[i]);
		}
	}

	@Override
	public int getColumnCount() {
		return mColumnNames.size();
	}

	@Override
	public int getRowCount() {
		return mTableData.size();
	}

	@Override
	public Object getValueAt(int row, int column) {
		return mTableData.get(row).get(column);
	}

	public String getColumnName(int column) {
		return mColumnNames.get(column);
	}

	@Override
	public boolean isCellEditable(int row, int column) {
		return false;
	}

	@Override
	public void setValueAt(Object obj, int row, int column) {

	}

	@Override
	public Class<?> getColumnClass(int col) {
		return (Class<?>) mColumnTypes.get(col);
	}

	public void setTableData(ArrayList<ArrayList<Object>> tableData) {
		mTableData = tableData;
	}
}

Соединение с БД

Соединение у нас одно, поэтому класс выполнил с помощью Singleton. Создать соединение можно через метод getInstance(), который подключается к БД с заданным логином/ паролем/ путем до файла FDB с помощью метода connect(). Для нашей модели мы будем брать данные с помощь метода getData(String sql). Также когда нам соединение больше не требуется его требуется закрыть, для этого используем метод release().
public class DBHelper {
	private Connection dbConnection;
	private static DBHelper sDBHelper ;
	private static final String DRIVER = "org.firebirdsql.jdbc.FBDriver";
	private static final String URL = "jdbc:firebirdsql:localhost/3050:C:\\DB\\DB.FDB";
	private static final String LOGIN = "SYSDBA";
	private static final String PASSWORD = "masterkey";

	public static synchronized DBHelper getInstance() {
		if (sDBHelper  == null) {
			sDBHelper  = new DBHelper ();
		}
		return sDBHelper ;
	}

	private DBHelper () {
	}

	public void connect() {
		try {
			Class.forName(DRIVER);
			dbConnection = DriverManager.getConnection(URL, LOGIN, PASSWORD);
		} catch (ClassNotFoundException ex) {
			ex.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	public PreparedStatement getPrepareStatement(String sql)
			throws SQLException {
		return dbConnection.prepareStatement(sql);
	}

	public synchronized ArrayList<ArrayList<Object>> getData(String query) {
		ArrayList<ArrayList<Object>> dataVector = new ArrayList<ArrayList<Object>>();
		Statement st = null;
		ResultSet rs = null;
		try {
			st = dbConnection.createStatement();
			rs = st.executeQuery(query);
			int columns = rs.getMetaData().getColumnCount();

			while (rs.next()) {
				ArrayList<Object> nextRow = new ArrayList<Object>(columns);
				for (int i = 1; i <= columns; i++) {
					nextRow.add(rs.getObject(i));
				}
				dataVector.add(nextRow);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			if (rs != null) {
				try {
					rs.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			if (st != null) {
				try {
					st.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
		}
		return dataVector;
	}

	public void release() {
		if (sDBHelper  != null) {
			sDBHelper  = null;
		}
		if (dbConnection != null) {
			try {
				dbConnection.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}
}

Создание компонент

Нам потребуются такие компоненты как JTable, JScrollPane, JComboBox, JLabel, JTextField, JButton. Сделан класс во избежание бесполезного копирования кода создания компонент при создании таблиц.
public class Components {
	public static AbsTable createTableModel(Class<?>[] types, String[] col,
			String sql) {
		AbsTable table = new AbsTable(types, col);
		table.setTableData(DBHelper .getInstance().getData(sql));
		return table;
	}

	public static JTable createTable(AbsTable model) {
		JTable table = new JTable(model);
		table.getColumnModel().getColumn(0).setMaxWidth(50);
		return table;
	}

	public static JScrollPane createScroll(JTable table) {
		return new JScrollPane(table);
	}

	public static JComboBox<String> createCombo(String[] items,
			ItemListener listener) {
		JComboBox<String> combo = new JComboBox<String>(items);
		combo.setEditable(false);
		combo.setSelectedIndex(-1);
		combo.addItemListener(listener);
		return combo;
	}

	public static JLabel createLabel(String name) {
		JLabel label = new JLabel(name);
		label.setHorizontalTextPosition(JLabel.LEFT);
		label.setIconTextGap(5);
		label.setForeground(Color.black);
		return label;
	}

	public static JTextField createEdit(String text) {
		JTextField tf= new JTextField(text);
		tf.setEditable(true);
		tf.setForeground(Color.black);
		return tf;
	}

	public static JButton createButton(String name, ActionListener listener) {
		JButton button = new JButton(name);
		button.addActionListener(listener);
		return button;
	}
}

Вывод таблиц на фрэйм и интерфейс работы с таблицей

Мы уже определили что обязанности интерфейса это вывод таблицы, добавление\обновление\удаление\изменение таблицы. Поэтому создадим базовый абстрактный класс BaseFrame, наследованный от JFrame.
Исходя из обязанностей все таблицы должны обновляться, обеспечивать добавление\удаление\изменение записей, поэтому методы add(), delete(), save(), updateTable делаем абстрактными для всех таблиц. Также в базовом классе должна быть ссылка на соединение(это у нас DBHelper ).
На фрэйме у нас будет
— таблица JTable
— кнопки JButton добавить\сохранить\удалить запись
— скролл JScrollPane для большого числа записей
Все это одинаково для всех создаваемых таблиц поэтому располагается в базовом классе. С класса Components создаем кнопки и назначаем им листенеры(слушатели) действий. Также расширяем базовый класс с помощью интерфейса ListSelectionListener, чтобы ловить события нажатия на ячейки нашей таблицы.
abstract class BaseFrame extends JFrame implements ListSelectionListener {
	protected JButton mDeleteBtn;
	protected JButton mAddBtn;
	protected JButton mSaveBtn;
	protected JPanel mControlArea;
	protected JPanel mEditArea;
	protected JScrollPane mScroll;
	protected JTable mTable;

	protected Container mContainer;
	protected AbsTable mTableModel;

	protected DBHelper sDBHelper ;
	private static final int SIZE_X = 300;
	private static final int SIZE_Y = 450;

	public BaseFrame(String name) {
		super(name);
		sDBHelper  = DBHelper .getInstance();
		sBDHelper.connect();

		mAddBtn = Components.createButton(Strings.ADD, new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				add();
			}
		});
		mDeleteBtn = Components.createButton(Strings.DELETE,
				new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						delete();
					}
				});
		mSaveBtn = Components.createButton(Strings.SAVE, new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				save();
			}
		});
		setSize(new Dimension(SIZE_X, SIZE_Y));
		setVisible(true);
		setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
	}

	abstract void updateTable();

	abstract void add();

	abstract void delete();

	abstract void save();
}

Наконец создаем нашу таблицу. Для её редактирования понадобится 2 JTextField и 2 JLabel. Создаем наши компоненты с помощью класса Component. Добавляем компоненты на фрэйм. Далее требуется переопределить абстрактные методы базового класса для работы с БД(добавление\изменение\удаление записей). Для написания этих методов потребуется немного знания SQL. Не забываем обновлять интерфейс таблицы с помощью переопределенного метода updateTable. Также переопределяем метод valueChanged(..) для обработки нажатия по ячейке.
public class RanksFrame extends BaseFrame {
	private JLabel mIdLabel;
	private JLabel mRankLabel;
	private JTextField mIdEdit;
	private JTextField mRankEdit;

	public RanksFrame() {
		super(Strings.RANK);
		mContainer = getContentPane();

		mTableModel = Components.createTableModel(Tables.RANKS_TYPE,
				Tables.RANKS_TABLE, "SELECT * FROM RANKS ORDER BY ID");
		mTable = Components.createTable(mTableModel);
		mScroll = Components.createScroll(mTable);

		mIdLabel = Components.createLabel(Strings.ID);
		mIdEdit = Components.createEdit("");

		mRankLabel = Components.createLabel(Strings.RANK);
		mRankEdit = Components.createEdit("");

		mTable.getSelectionModel().addListSelectionListener(this);
		mControlArea = new JPanel(new GridLayout(1, 3));
		mEditArea = new JPanel(new GridLayout(2, 2));

		mEditArea.add(mIdLabel);
		mEditArea.add(mIdEdit);
		mEditArea.add(mRankLabel);
		mEditArea.add(mRankEdit);

		mControlArea.add(mSaveBtn);
		mControlArea.add(mDeleteBtn);
		mControlArea.add(mAddBtn);

		mContainer.add(mScroll);
		mContainer.add(mEditArea);
		mContainer.add(mControlArea);
		mContainer.setLayout(new BoxLayout(mContainer, BoxLayout.Y_AXIS));
	}

	@Override
	public void updateTable() {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				mTableModel.setTableData(sBDHelper
						.getData("SELECT * FROM RANKS ORDER BY ID"));
				mTable.updateUI();
				mRankEdit.setText(null);
				mIdEdit.setText(null);
			}
		});
	}

	@Override
	public void add() {
		PreparedStatement ps = null;
		try {
			ps = sBDHelper
					.getPrepareStatement("INSERT INTO RANKS (ID,RANK) VALUES(?,?)");
			ps.setString(1, mIdEdit.getText());
			ps.setString(2, mRankEdit.getText());
			ps.executeUpdate();
		} catch (SQLException r) {
			r.printStackTrace();
		} finally {
			if (ps != null) {
				try {
					ps.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			updateTable();
		}
	}

	@Override
	public void delete() {
		PreparedStatement ps = null;
		try {
			ps = sBDHelper.getPrepareStatement("DELETE FROM RANKS WHERE ID=?");
			ps.setString(1, mIdEdit.getText());
			ps.executeUpdate();
		} catch (SQLException r) {
			r.printStackTrace();
		} finally {
			if (ps != null) {
				try {
					ps.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			updateTable();
		}
	}

	@Override
	public void save() {
		PreparedStatement ps = null;
		try {
			ps = sBDHelper
					.getPrepareStatement("UPDATE RANKS SET RANK=? WHERE ID=?");
			ps.setString(1, mRankEdit.getText());
			ps.setString(2, mIdEdit.getText());
			ps.executeUpdate();
		} catch (SQLException r) {
			r.printStackTrace();
		} finally {
			if (ps != null) {
				try {
					ps.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			updateTable();
		}
	}

	@Override
	public void valueChanged(ListSelectionEvent e) {
		mIdEdit.setText(mTable.getModel()
				.getValueAt(mTable.getSelectedRow(), 0).toString());
		mRankEdit.setText(mTable.getModel()
				.getValueAt(mTable.getSelectedRow(), 1).toString());
	}
}

Конец близок, интерфейс выбора таблицы

Пишем код главного окна БД. Интерфейс выбора выглядит как JComboBox с названиями таблиц mTables. По нажатию срабатывает switch к выбранной таблицы, здесь нужно будет добавлять все наши таблицы для их создания. Ох, наконец, для обработки закрытия нашего приложения используем интерфейс WindowListener(написал только метод который использую, остальные выкинул ибо итак много кода), при закрытии закрываем соединение.
public class Application extends JFrame implements WindowListener {
	private String[] mTables = { "Ranks" };
	private static final int RANKS = 0;

	private JComboBox<String> mComboMenu;

	public Application() throws SQLException {
		super(Strings.DB_NAME);
		mComboMenu = Components.createCombo(mTables,
				new ItemListener() {
					@Override
					public void itemStateChanged(ItemEvent evt) {
						switch (mComboMenu.getSelectedIndex()) {
						case RANKS:
							new RanksFrame();
							break;
						}
						SwingUtilities.invokeLater(new Runnable() {
							public void run() {
								mComboMenu.setSelectedIndex(-1);
							}
						});
					}
				});
		Container container = getContentPane();
		container.add(mComboMenu);
		container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS));

		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setSize(400, 80);
		setResizable(false);
		setVisible(true);
		addWindowListener(this);
	}

	public static void main(String[] args) throws SQLException {
		new Application();
	}

	@Override
	public void windowClosing(WindowEvent arg0) {
		DBHelper .getInstance().release();
	}
}

Заключение


Надеюсь не утомил, статья носит обучающий характер, поэтому надеюсь написание/создание таблиц БД после прочтения данной статьи облегчится. Также преследую призрачную надежду того, что студенты, наконец, сядут и напишут сами базу данных.

P.S. Надеюсь мой рефакторинг удался и все выглядит просто и наглядно. Критика приветствуется, особенно по шаблонам проектирования.
upd:
Vector -> ArrayList спасибо javax
За множество недочетов спасибо gvsmirnov и aleksandy
Tags:
Hubs:
Total votes 17: ↑6 and ↓11-5
Comments26

Articles