Firebird BD на Java

в 18:07, , рубрики: eclipse, firebird, java, swing, базы данных, Песочница, метки: , , , ,

Вступление

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

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

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

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

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

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

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

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

Для написания интерфейса выбрал Swing.
Обязанности интерфейса будут такие
— Выбор таблицы
— Вывод Обновление таблицы
— Добавление Удаление Вставка записи
В итоге у нас будет вот такой интерфейс
Firebird BD на Java
Архитектуру приложения представляю следующим образом
— главный класс с main(..) (Application)
— соединение с нашей БД (BDHelper)
— общая модель для любой таблицы (AbsTable, Tables)
— базовый класс для всех таблиц(BaseFrame) и классы таблиц наследников (RankFrame)
— класс, создающий компоненты (ComponentsFactory)
— вспомогательный класс для строк, навеяно андроидом (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 Vector<String> mColumnNames;
	private Vector<Vector<Object>> mTableData;
	private Vector<Object> mColumnTypes;

	public AbsTable(Class<?>[] types, String[] columns) {
		super();
		mColumnTypes = new Vector<Object>(types.length);
		mColumnNames = new Vector<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(Vector<Vector<Object>> tableData) {
		mTableData = tableData;
	}
}

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

Соединение у нас одно, поэтому класс выполнил с помощью Singleton(Объяснять что это нет смысла, полно статей на эту тему). Создать соединение можно с помощью метода create(), который подключается к БД с заданным логином/ паролем/ путем до файла FDB. Для нашей модели мы будем брать данные с помощь метода getData(String sql). Также когда нам соединение больше не требуется его требуется закрыть, для этого используем метод release().

public class BDHelper {
	private Connection dbConnection;
	private static BDHelper sBDHelper;
	private static final String DRIVER = "org.firebirdsql.jdbc.FBDriver";
	private static final String URL = "jdbc:firebirdsql:localhost/3050:C:\BD\BD.FDB";
	private static final String LOGIN = "SYSDBA";
	private static final String PASSWORD = "masterkey";

	public static synchronized BDHelper getInstance() {
		return sBDHelper;
	}

	public static synchronized BDHelper create() {
		if (sBDHelper == null) {
			sBDHelper = new BDHelper();
		}
		return sBDHelper;
	}

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

	public synchronized Connection getConnection() {
		return dbConnection;
	}

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

	public synchronized Vector<Vector<Object>> getData(String query) {
		Vector<Vector<Object>> dataVector = new Vector<Vector<Object>>();
		try {
			Statement st = dbConnection.createStatement();
			ResultSet rs = st.executeQuery(query);
			int columns = rs.getMetaData().getColumnCount();
			
			while (rs.next()) {
				Vector<Object> nextRow = new Vector<Object>(columns);
				for (int i = 1; i <= columns; i++) {
					nextRow.add(rs.getObject(i));
				}
				dataVector.add(nextRow);
			}
			rs.close();
			st.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return dataVector;
	}

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

Фабрика компонент

Будем использовать шаблон фабрика для создания наших компонент, Нам потребуются такие компоненты как JTable, JScrollPane, JComboBox, JLabel, JTextField, JButton. Сделан класс во избежание бесполезного копирования кода создания компонент при создании таблиц. Фабрика у нас одна поэтому опять используем Singleton. Для уничтожения фабрики используем как всегда release().

public class ComponentsFactory {
	private static ComponentsFactory sFactory;

	public static synchronized ComponentsFactory getInstance() {
		return sFactory;
	}

	public static synchronized ComponentsFactory create() {
		if (sFactory == null) {
			sFactory = new ComponentsFactory();
		}
		return sFactory;
	}

	private ComponentsFactory() {

	}

	public AbsTable createTable(Class<?>[] types, String[] col, String sql) {
		AbsTable table = new AbsTable(types, col);
		table.setTableData(BDHelper.getInstance().getData(sql));
		return table;
	}

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

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

	public 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 JLabel createLabel(String name) {
		JLabel label = new JLabel(name);
		label.setHorizontalTextPosition(JLabel.LEFT);
		label.setIconTextGap(5);
		label.setForeground(Color.black);
		return label;
	}

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

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

	public void release() {
		if (sFactory != null) {
			sFactory = null;
		}
	}
}

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

Мы уже определили что обязанности интерфейса это вывод таблицы, добавлениеобновлениеудалениеизменение таблицы. Поэтому создадим базовый абстрактный класс BaseFrame, наследованный от JFrame.
Исходя из обязанностей все таблицы должны обновляться, обеспечивать добавлениеудалениеизменение записей, поэтому методы add(), delete(), save(), updateTable делаем абстрактными для всех таблиц. Также в базовом классе должна быть ссылка на соединение(это у нас BDHelper). Таблицы будут иметь ряд компонент на фрэйме, поэтому нам нужна ссылка на фабрику, для создания. На фрэйме у нас будет
— таблица JTable
— кнопки JButton добавитьсохранитьудалить запись
— скролл JScrollPane для большого числа записей
Все это одинаково для всех создаваемых таблиц поэтому располагается в базовом классе. С помощью фабрики создаем кнопки и назначаем им листенеры(слушатели) действий. Также расширяем базовый класс
с помощью интерфейса 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 BDHelper sBDHelper;
	protected ComponentsFactory mComponentsFactory;
	private static final int SIZE_X = 300;
	private static final int SIZE_Y = 450;

	public BaseFrame(String name) {
		super(name);
		sBDHelper = BDHelper.getInstance();
		mComponentsFactory = ComponentsFactory.getInstance();

		mAddBtn = mComponentsFactory.createButton(Strings.ADD,
				new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						add();
					}
				});
		mDeleteBtn = mComponentsFactory.createButton(Strings.DELETE,
				new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						delete();
					}
				});
		mSaveBtn = mComponentsFactory.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. Создаем наши компоненты с помощью фабрики. Добавляем компоненты на фрэйм. Далее требуется переопределить абстрактные методы базового класса для работы с БД(добавлениеизменениеудаление записей). Для написания этих методов потребуется немного знания 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 = mComponentsFactory.createTable(Tables.RANKS_TYPE,
				Tables.RANKS_TABLE, "SELECT * FROM RANKS ORDER BY ID");
		mTable = mComponentsFactory.createTable(mTableModel);
		mScroll = mComponentsFactory.createScroll(mTable);

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

		mRankLabel = mComponentsFactory.createLabel(Strings.RANK);
		mRankEdit = mComponentsFactory.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() {
		mTableModel.setTableData(sBDHelper
				.getData("SELECT * FROM RANKS ORDER BY ID"));
		mTable.updateUI();
		mRankEdit.setText(null);
		mIdEdit.setText(null);
	}

	@Override
	public void add() {
		try {
			PreparedStatement 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 {
			updateTable();
		}
	}

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

	@Override
	public void save() {
		try {
			PreparedStatement 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 {
			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());
	}
}

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

Пишем код главного окна БД. Т.к. это входная точка в приложение, то создадим нашу фабрику и установим соединение с БД с помощью методом create(). Интерфейс выбора выглядит как 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.BD_NAME);
		BDHelper.create();
		ComponentsFactory.create();
		mComboMenu = ComponentsFactory.getInstance().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) {
		BDHelper.getInstance().release();
		ComponentsFactory.getInstance().release();
	}
}

Заключение

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

P.S. Надеюсь мой рефакторинг удался и все выглядит просто и наглядно. Критика приветствуется, особенно по шаблонам проектирования.

Автор: t0xic

  1. Ololo:

    А класс Strings?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js