Birbaşa mətləbə keçəcəm. Deməli, iş yoldaşıma proyektdə belə bir şey lazım oldu; web səhifəsi var idi, həmin web səhifənin menusunu admin panelində aydın və anlaşıqlı şəkildə göstərmək lazım idi ki, yeni menu/alt-menu əlavə etmək, mövcud menunu silmək yaxud menularda dəyişikliklər etmək daha rahat olsun. Və bu məsələ üçün optimal həll variantı fikirləşmək lazım idi.
Bu məsələni, həmin web səhifə əvəzinə öz blogumdan nümunə verməklə göstərəcəm və kodlaşdırılmasına da bu nümunə üzərindən davam edəcəyik.
Deməli mənim blogumun menusu təxminən belədir:
Və indi bizə lazımdır ki, menu admin paneldə aşağıdakı formada görünsün:
MENU 1 Home 2 Certification Exam 2.1 Exam preparation: step by step 2.2 Exam Topics 2.3 Exam Experience 3 Sample questions 3.1 Oracle 3.1.1 1Z0-803 (OCA 7) 3.1.2 1Z0-804 (OCP 7) 3.1.3 1Z0-805 (Upgrade to Java SE 7) 3.1.4 1Z0-808 (OCA 8) 3.1.5 1Z0-809 (OCP 8) 3.1.6 1Z0-810 (Upgrade SE 7 to 8 OCP) 3.1.7 1Z0-813 (Upgrade to SE 8 OCP) 4 My Books 4.1 My Certification Notes 5 Articles 6 News 7 Mix 8 Feedbacks 9 About
Menu ilə bağlı məlumatlar verilənlər bazasında aşağıdakı formada saxlanılır:
CREATE TABLE IF NOT EXISTS `menu` ( `id` int(11) NOT NULL, `parent_id` int(11) NOT NULL, `name` varchar(50) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
id | parent_id | name |
0 | -1 | MENU |
1 | 0 | Home |
2 | 0 | Certification Exam |
3 | 0 | Sample questions |
4 | 0 | My Books |
5 | 0 | Articles |
6 | 0 | News |
7 | 0 | Mix |
8 | 0 | Feedbacks |
9 | 0 | About |
10 | 2 | Exam preparation: step by step |
11 | 2 | Exam Topics |
12 | 2 | Exam Experience |
13 | 3 | Oracle |
14 | 13 | 1Z0-803 (OCA 7) |
15 | 13 | 1Z0-804 (OCP 7) |
16 | 13 | 1Z0-805 (Upgrade to Java SE 7) |
17 | 13 | 1Z0-808 (OCA 8) |
18 | 13 | 1Z0-809 (OCP 8) |
19 | 13 | 1Z0-810 (Upgrade SE 7 to 8 OCP) |
20 | 13 | 1Z0-813 (Upgrade to SE 8 OCP) |
21 | 4 | My Certification Notes |
Menu və alt-menuların sayı dəyişkəndir, hər vaxt artıb-azala bilər. Hər yeni alt-menu əlavə edildikcə, parent_id
kimi həmin alt-menunun aid olduğu menunun id
dəyəri əlavə edilir. id
dəyəri isə hər menu üçün unikaldır, təkrarlanmır. Ona görə də dinamik bir həll üsulu tapmaq lazım idi. Biz buna DFS alqoritmini tətbiq etdik (“Preorder Tree Walk” versiyası da ağlıma gəldi, amma onu araşdırmadım, kimin başqa maraqlı həll variantı olsa qeyd edə bilər). DFS alqoritmi ilə bağlı detallara çox getməyəcəm, sadəcə kod üzərində tətbiqini göstərməyə çalışacam. Qısaca DFS (Depth-First-Search) alqoritmi qraflar üzərində ən çox istifadə edilən axtarış alqoritmlərindən biridir:
Biz bu alqoritm vasitəsilə menunun alt menularını axtaracağıq.
Sual yarana bilər ki, bəs bu menunun qrafla nə əlaqəsi var yaxud necə əlaqələndirəcəyik? Biz əgər məlumat bazasında mövcud olan menumuzu qraf üzərində təsvir etsək aşağıdakı mənzərə alınacaq:
Amma problemi qraf şəklinə salıb həll etmək istəyiriksə, dəyərləri saxlamaq üçün qraflara uyğun data strukturlardan istifadə etməliyik. Adətən iki cür data struktur istifadə edilir:
- Adjacency Matrix
- Adjacency List
Biz “adjacency list” istifadə edəcəyik, amma bir az daha optimallaşdırılmış formada. Adjacency list üçün adətən massiv istifadə edilir, massivin elementləri LinkedList
`lərdən ibarət olur. Amma bizdə məlumat daha dinamikdir, məlumat bazasında id
sütununun dəyəri ardıcıl artmaya da bilər yaxud hər hansı id
silindikdə fərqli mənzərə yarana bilər. Yəni, qısaca desək id
`nin dəyəri menunun sayından bir neçə dəfə böyük bir ədəd də ola bilər. Bu baxımdan massiv istifadə etmək optimal deyil, əvəzində Map
`lərdən istifadə edəcəyik. Kod üzərindən baxdıqda daha aydın olacaq.
Proyektin strukturu:
Main.java
package az.mm.displaymenu; import java.util.List; import java.util.Map; /** * * @author MM <[email protected]> */ public class Main { private final Map<Integer, List<Submenu>> adjMap; public Main() { adjMap = DBConnection.getMenu(); } public static void main(String[] args) { Main m = new Main(); m.displayMenu(0, "", "", "MENU"); } void displayMenu(int pId, String padding, String rowNum, String name) { System.out.printf("%s%s %s%n", padding, rowNum, name); int row = 1; if(adjMap.get(pId) != null) for (Submenu s: adjMap.get(pId)) if (!s.isVisited()) { s.setVisited(true); String newPadding = padding + "\t"; String newRowNum = "".equals(rowNum) ? String.valueOf(row++) : (rowNum + "." + row++); displayMenu(s.to(), newPadding, newRowNum, s.name()); } } }
DBConnection.java
package az.mm.displaymenu; import java.io.*; import java.sql.*; import java.util.*; /** * * @author MM <[email protected]> */ public class DBConnection { static { createMenu(); } private static Connection getDBConnection() { Connection connection = null; try { DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver()); connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/?user=root&password=root&serverTimezone=UTC"); } catch (SQLException e) { System.err.println(e); } return connection; } private static void createMenu(){ try (Connection connection = getDBConnection(); BufferedReader br = new BufferedReader(new FileReader("src/main/resources/create.sql")); ) { String str; StringBuilder sb = new StringBuilder(); while ((str = br.readLine()) != null) sb.append(str); for(String sql: sb.toString().split(";")) try(PreparedStatement preparedStatement = connection.prepareStatement(sql);){ preparedStatement.execute(); } } catch (Exception e) { System.err.println(e); } } public static Map<Integer, List<Submenu>> getMenu() { Map<Integer, List<Submenu>> adjMap = new LinkedHashMap(); String sql = "Select * from test.menu;"; try (Connection connection = getDBConnection(); PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet rs = preparedStatement.executeQuery();) { while (rs.next()) { Submenu s = new Submenu(rs.getInt("parent_id"), rs.getInt("id"), rs.getString("name")); if (adjMap.containsKey(s.from())) adjMap.get(s.from()).add(s); else { List<Submenu> menuList = new LinkedList(); menuList.add(s); adjMap.put(s.from(), menuList); } } } catch (SQLException e) { System.err.println(e); } return adjMap; } }
Submenu.java
package az.mm.displaymenu; /** * * @author MM <[email protected]> */ public class Submenu { private final int from; private final int to; private final String name; private boolean visited; public Submenu(int from, int to, String name) { this.from = from; this.to = to; this.name = name; } public int from() { return from; } public int to() { return to; } public String name() { return name; } public boolean isVisited() { return visited; } public void setVisited(boolean visited) { this.visited = visited; } }
create.sql
CREATE DATABASE IF NOT EXISTS `test`; USE `test`; CREATE TABLE IF NOT EXISTS `menu` ( `id` int(11) NOT NULL, `parent_id` int(11) NOT NULL, `name` varchar(50) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; DELETE FROM `menu`; INSERT INTO `menu` (`id`, `parent_id`, `name`) VALUES (0, -1, 'MENU'), (1, 0, 'Home'), (2, 0, 'Certification Exam'), (3, 0, 'Sample questions'), (4, 0, 'My Books'), (5, 0, 'Articles'), (6, 0, 'News'), (7, 0, 'Mix'), (8, 0, 'Feedbacks'), (9, 0, 'About'), (10, 2, 'Exam preparation: step by step'), (11, 2, 'Exam Topics'), (12, 2, 'Exam Experience'), (13, 3, 'Oracle'), (14, 13, '1Z0-803 (OCA 7)'), (15, 13, '1Z0-804 (OCP 7)'), (16, 13, '1Z0-805 (Upgrade to Java SE 7)'), (17, 13, '1Z0-808 (OCA 8)'), (18, 13, '1Z0-809 (OCP 8)'), (19, 13, '1Z0-810 (Upgrade SE 7 to 8 OCP)'), (20, 13, '1Z0-813 (Upgrade to SE 8 OCP)'), (21, 4, 'My Certification Notes');
pom.xml
... <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.11</version> </dependency> ....
Proyektin github linki:
https://github.com/mmushfiq/display-website-menu-using-dfs-algorithm