简介:这是一套面向Java初学者与数据库入门实践者的简易数据库系统源码,聚焦基础CRUD操作与前后端协同实现,适合课程设计、小型项目原型开发或数据库原理教学辅助。资源共29个文件,压缩包仅181KB,包含13个Java核心类(如Interpreter、Buffer_Manager、Record_Manager等),支撑数据库解析、缓存管理、记录操作等关键逻辑;11个XML配置文件用于定义数据库连接、IDEA项目结构及UI组件参数;另有HTML前端界面、SQL建库脚本、LICENSE授权文件、README说明文档及IntelliJ IDEA项目配置文件,形成完整可运行闭环。已有304人学习下载,代码结构清晰、模块职责分明,配套SQL初始化脚本与详细配置说明,便于快速导入IDE运行调试,是理解嵌入式数据库底层机制与Web交互逻辑的优质实践材料。
1. 为什么一个“简易数据库系统”要用 Java + HTML 而不是直接上 MySQL 或 SQLite?
这不是在造轮子,而是在拆解数据库的“黑匣子”——当你用 Java 写出一个能建表、插数据、查记录、删字段的控制台+网页交互系统,你才真正看清 CREATE TABLE 背后是内存结构体还是文件偏移量,SELECT WHERE 不是 SQL 解析器 magic,而是你亲手写的字符串匹配+循环过滤。这个“基于 Java 与 HTML 的简易数据库系统设计源码”,本质是一套可调试、可打断点、可单步追踪的数据库教学沙盒:Java 负责核心逻辑(数据存储、CRUD、事务模拟)、HTML 提供轻量前端界面(避免 Swing/AWT 的过时感和复杂度),二者通过嵌入式 HTTP 服务(如 Jetty 或自研 Socket Server)桥接。它不替代生产级数据库,但能让你在面试被问到“事务隔离级别怎么实现”时,不是背八股文,而是指着自己写的lockMap.put(table, new ReentrantLock())说:“我试过加锁粒度从行级降到页级,性能掉 40%,但幻读真没了”。适合 Java 初学者巩固集合/IO/多线程,也适合数据库原理课学生把 ER 图、范式、B+树索引这些抽象概念,落到HashMap<String, List<Row>>和FileChannel.write()的真实字节里。
2. 核心架构:三层分离不是口号,是必须动手画清的数据流
这个简易系统绝不能写成“一个 Java 类塞满所有逻辑 + 一个 HTML 表单硬编码提交”。必须强制分层,否则后续加索引、改存储引擎、换前端框架会直接翻车。我一般会按以下三块组织源码结构,每层职责清晰、接口明确:
2.1 数据层(Data Layer):用纯 Java 实现“表”的最小可行模型
核心是Table类,它不依赖任何第三方库,只用 JDK 原生类:
public class Table { private final String name; private final List<String> columns; // 字段名列表,顺序即列序 private final List<Map<String, Object>> rows; // 每行是 Map<字段名, 值> private final Map<String, Integer> columnIndices; // 字段名 → 列索引,加速 get() public Table(String name, List<String> columns) { this.name = name; this.columns = new ArrayList<>(columns); this.rows = new ArrayList<>(); this.columnIndices = new HashMap<>(); for (int i = 0; i < columns.size(); i++) { columnIndices.put(columns.get(i), i); } } public void insert(Map<String, Object> row) { // 验证字段数一致、类型暂不校验(简易版) if (row.size() != columns.size()) { throw new IllegalArgumentException("Column count mismatch"); } rows.add(new HashMap<>(row)); // 深拷贝防外部修改 } public List<Map<String, Object>> select(Map<String, Object> conditions) { List<Map<String, Object>> result = new ArrayList<>(); for (Map<String, Object> row : rows) { boolean match = true; for (Map.Entry<String, Object> cond : conditions.entrySet()) { String col = cond.getKey(); Object val = cond.getValue(); if (!Objects.equals(row.get(col), val)) { match = false; break; } } if (match) result.add(new HashMap<>(row)); } return result; } }关键设计说明:
columns用List<String>而非Set,保证列序稳定(影响 INSERT 语句解析和显示顺序);rows存Map<String, Object>而非Object[],牺牲一点内存换字段名可读性,方便 HTML 表格渲染;columnIndices是性能关键:避免每次select都遍历columns找索引,O(1) 定位;insert中new HashMap<>(row)是血泪经验——不深拷贝,前端传来的row被后续修改会污染存储。
2.2 服务层(Service Layer):暴露 REST-like 接口,屏蔽底层细节
用 Java 自带HttpServer(JDK 18+)或嵌入 Jetty(兼容老 JDK),定义/api/table/{name}等路径。重点不是做完整 REST,而是让 HTML 表单能fetch('/api/insert', {method:'POST', body: JSON.stringify(...)})直接调用:
// 示例:处理 POST /api/insert 请求 private void handleInsert(HttpExchange exchange) throws IOException { String requestBody = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); JsonObject json = JsonParser.parseString(requestBody).getAsJsonObject(); String tableName = json.get("table").getAsString(); JsonObject data = json.getAsJsonObject("data"); Table table = database.getTable(tableName); // database 是全局 TableRegistry Map<String, Object> row = new HashMap<>(); for (Map.Entry<String, JsonElement> entry : data.entrySet()) { // 简单类型转换:字符串、数字、布尔值 JsonElement value = entry.getValue(); if (value.isJsonNull()) { row.put(entry.getKey(), null); } else if (value.isJsonPrimitive()) { JsonPrimitive p = value.getAsJsonPrimitive(); if (p.isBoolean()) row.put(entry.getKey(), p.getAsBoolean()); else if (p.isNumber()) row.put(entry.getKey(), p.getAsNumber()); else row.put(entry.getKey(), p.getAsString()); } } table.insert(row); exchange.sendResponseHeaders(200, 0); OutputStream os = exchange.getResponseBody(); os.write("OK".getBytes(StandardCharsets.UTF_8)); os.close(); }参数说明:
requestBody必须用StandardCharsets.UTF_8显式指定编码,否则中文字段名/值会乱码;JsonParser来自com.google.gson(需引入依赖),比原生JSONObject更健壮;table.insert(row)是唯一调用数据层的地方,服务层绝不碰rows列表——这是分层铁律。
2.3 表示层(Presentation Layer):HTML + 原生 JS,拒绝框架绑架
前端不用 Vue/React,就用最朴素的<form>+fetch。关键在于表单动态生成:用户输入CREATE TABLE user (id INT, name VARCHAR(20))后,JS 解析字段名和类型,生成对应 input:
<!-- 动态生成的插入表单 --> <form id="insertForm" onsubmit="return handleSubmit(event)"> <input type="hidden" name="table" value="user"> <div class="field-group"> <label>id:</label> <input type="number" name="id" required> </div> <div class="field-group"> <label>name:</label> <input type="text" name="name" maxlength="20" required> </div> <button type="submit">插入记录</button> </form> <script> function handleSubmit(e) { e.preventDefault(); const form = e.target; const data = new FormData(form); const obj = {}; for (let [key, value] of data.entries()) { if (key !== 'table') obj[key] = value; // 过滤隐藏字段 } fetch('/api/insert', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ table: form.querySelector('[name="table"]').value, data: obj }) }) .then(r => r.text()) .then(text => { if (text === 'OK') { alert('插入成功'); form.reset(); } else { alert('插入失败:' + text); } }); return false; } </script>为什么坚持原生 JS?
- 面试官看你源码时,一眼能看懂逻辑,不被
v-model或useState干扰;- 调试时
console.log(obj)直接看到提交数据,没有框架中间层遮挡;FormData自动处理空值/类型,比手写JSON.stringify({id: document.getElementById('id').value})更鲁棒。
3. 存储持久化:别让数据重启就消失,文件存取的 3 种落地方式
内存存储(List<Table>)只是起点,真正的“数据库”必须落盘。这里提供三种渐进式方案,按可靠性排序,强烈建议从方案一做起,再逐步升级:
3.1 方案一:CSV 文件存储(新手友好,调试直观)
每个表存为一个 CSV 文件,如user.csv,首行为字段名,后续为数据行。Java 用Files.write()写,Files.readAllLines()读:
public void saveToCsv(Table table, Path dir) throws IOException { Path file = dir.resolve(table.getName() + ".csv"); List<String> lines = new ArrayList<>(); // 第一行:字段名,用逗号分隔 lines.add(String.join(",", table.getColumns())); // 后续行:每行数据,字符串转义(简单处理:双引号包裹含逗号的值) for (Map<String, Object> row : table.getRows()) { List<String> values = new ArrayList<>(); for (String col : table.getColumns()) { Object val = row.get(col); String str = (val == null) ? "" : val.toString(); // 简单转义:含逗号、换行、双引号的值用双引号包裹,内部双引号变两个 if (str.contains(",") || str.contains("\n") || str.contains("\"")) { str = "\"" + str.replace("\"", "\"\"") + "\""; } values.add(str); } lines.add(String.join(",", values)); } Files.write(file, lines, StandardCharsets.UTF_8); }优势与边界:
- ✅ 用 Excel 或记事本直接打开查看,调试零门槛;
- ✅
Files.write()原子写入(JDK 7+),避免写一半崩溃;- ❌ 不支持并发写(多线程同时
saveToCsv会覆盖);- ❌ 无事务,
INSERT成功但saveToCsv失败,数据丢失;- 适用场景:单用户本地工具、课程实验、原型验证。
3.2 方案二:JSON 文件存储(支持嵌套,结构更清晰)
用 Gson 库将整个Table对象序列化为 JSON,每个表一个.json文件:
public void saveToJson(Table table, Path dir) throws IOException { Path file = dir.resolve(table.getName() + ".json"); JsonObject json = new JsonObject(); json.addProperty("name", table.getName()); json.add("columns", new Gson().toJsonTree(table.getColumns())); JsonArray rowsArray = new JsonArray(); for (Map<String, Object> row : table.getRows()) { JsonObject rowObj = new JsonObject(); for (Map.Entry<String, Object> entry : row.entrySet()) { Object val = entry.getValue(); if (val == null) { rowObj.add(entry.getKey(), JsonNull.INSTANCE); } else if (val instanceof Number) { rowObj.add(entry.getKey(), new JsonPrimitive((Number) val)); } else if (val instanceof Boolean) { rowObj.add(entry.getKey(), new JsonPrimitive((Boolean) val)); } else { rowObj.add(entry.getKey(), new JsonPrimitive(val.toString())); } } rowsArray.add(rowObj); } json.add("rows", rowsArray); Files.write(file, new Gson().toJson(json).getBytes(StandardCharsets.UTF_8)); }参数说明:
JsonNull.INSTANCE正确表示null,避免 Gson 默认忽略 null 字段;new Gson().toJson(json)生成紧凑 JSON,若要可读格式用new GsonBuilder().setPrettyPrinting().create();- 注意:JSON 不支持
Date、BigDecimal等类型,简易版只处理基础类型,符合教学定位。
3.3 方案三:SQLite 嵌入式存储(生产可用,无缝升级)
当 CSV/JSON 无法满足查询性能或并发需求时,替换数据层为 SQLite JDBC:
// 替换 Table 类为 SQLiteTable public class SQLiteTable { private final String tableName; private final Connection conn; // 共享 Connection,由 DatabaseManager 管理 public SQLiteTable(String tableName, Connection conn) { this.tableName = tableName; this.conn = conn; initTable(); // CREATE TABLE IF NOT EXISTS ... } public void insert(Map<String, Object> row) throws SQLException { String placeholders = String.join(",", Collections.nCopies(row.size(), "?")); String sql = "INSERT INTO " + tableName + " VALUES (" + placeholders + ")"; try (PreparedStatement ps = conn.prepareStatement(sql)) { int i = 1; for (Object val : row.values()) { if (val == null) ps.setNull(i, Types.NULL); else if (val instanceof Number) ps.setObject(i, val); else ps.setString(i, val.toString()); i++; } ps.executeUpdate(); } } }迁移要点:
- 只需重写
Table实现类,服务层和 HTML 层完全不动——这就是分层的价值;- SQLite JDBC 驱动 jar 包(如
sqlite-jdbc-3.45.1.0.jar)加入 classpath 即可;Connection必须池化(用 HikariCP)或至少复用,避免频繁创建连接开销。
4. 避坑指南:这 5 个错误让我重写了 3 次核心模块
写这个简易数据库时,我踩过的坑比代码行数还多。以下是高频、隐蔽、且修复成本高的 5 个问题,按“现象 → 原因 → 解决”列出,全是真实翻车现场:
4.1 现象:HTML 表单提交中文字段名,后端收到乱码(如??)
原因:HTTP 请求默认编码是 ISO-8859-1,而 HTML 页面声明了 UTF-8,但HttpExchange.getRequestBody()未指定 charset。
解决:在读取请求体时强制指定 UTF-8:
String requestBody = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);注意:
exchange.getRequestHeaders().getFirst("Content-Type")可能为空,不能依赖 header 判断编码。
4.2 现象:多次点击“插入”按钮,同一条数据被插了 3 次
原因:前端fetch成功后没禁用按钮,用户连续点击触发多次请求;后端无幂等校验。
解决:
- 前端:提交后立即
button.disabled = true,成功/失败后恢复; - 后端:对
INSERT请求加简单去重(如检查row的主键字段是否已存在),或用synchronized块包裹table.insert()(简易版够用)。
4.3 现象:SELECT * FROM user WHERE name='张三'返回空,但数据明明存在
原因:select方法中Objects.equals(row.get(col), val)比较的是String和JsonElement(Gson 解析后),类型不匹配永远为false。
解决:统一转换为String比较,或在conditions构建时就转好类型:
// 服务层解析条件时,统一转 String if (value.isJsonPrimitive()) { row.put(entry.getKey(), value.getAsString()); // 强制转字符串 }4.4 现象:CSV 文件保存后,Excel 打开显示所有内容挤在第一列
原因:Windows Excel 默认用逗号分隔,但若系统区域设置为“中文(中国)”,Excel 可能用分号分隔,或 CSV 缺少 BOM 头导致编码识别错误。
解决:
- 文件开头写入 UTF-8 BOM(
\uFEFF):Files.write(file, ("\uFEFF" + content).getBytes(StandardCharsets.UTF_8)); - 或改用制表符
\t分隔(更兼容),并保存为.tsv。
4.5 现象:Table对象被多个线程同时读写,偶尔抛ConcurrentModificationException
原因:ArrayList和HashMap非线程安全,rows.add(...)和for (Map row : rows)迭代同时发生必然崩溃。
解决:
- 读多写少场景:用
CopyOnWriteArrayList存rows,迭代安全,写操作稍慢但可接受; - 或更彻底:用
ReentrantReadWriteLock,读操作 lock.readLock(),写操作 lock.writeLock(); - 绝对不要用
Collections.synchronizedList()—— 它只同步单个方法,size()+get(i)组合仍不安全。
5. 查询优化实战:从 O(n) 全表扫描到 O(log n) 索引加速
简易系统默认select是全表扫描,1000 行耗时可忽略,但 10 万行就会卡顿。加索引不是炫技,而是理解 B+ 树本质的第一步。这里用最简方式实现单字段哈希索引,不引入外部库,50 行代码搞定:
5.1 设计索引结构:为user.id字段建唯一索引
在Table类中增加索引映射:
// Table.java 新增字段 private final Map<String, Map<Object, List<Integer>>> indexes; // 字段名 → (值 → 行号列表) public Table(String name, List<String> columns) { // ...原有初始化 this.indexes = new HashMap<>(); } public void createIndex(String columnName) { if (!columns.contains(columnName)) return; Map<Object, List<Integer>> indexMap = new HashMap<>(); for (int i = 0; i < rows.size(); i++) { Map<String, Object> row = rows.get(i); Object val = row.get(columnName); indexMap.computeIfAbsent(val, k -> new ArrayList<>()).add(i); } indexes.put(columnName, indexMap); }5.2 改写select方法:自动命中索引
public List<Map<String, Object>> select(Map<String, Object> conditions) { // 优先检查单条件且有索引的字段 if (conditions.size() == 1) { String col = conditions.keySet().iterator().next(); Object val = conditions.get(col); if (indexes.containsKey(col)) { List<Integer> rowIndices = indexes.get(col).get(val); if (rowIndices != null) { List<Map<String, Object>> result = new ArrayList<>(); for (Integer idx : rowIndices) { result.add(new HashMap<>(rows.get(idx))); } return result; } } } // 无索引或复合条件,退化为全表扫描 // ...原有逻辑 }5.3 在 HTML 界面暴露索引管理功能
加一个简单的索引管理表单,让用户手动建索引(教学意义大于实用):
<h3>创建索引</h3> <form onsubmit="createIndex(event)"> <select name="table" required> <option value="">选择表</option> <!-- 后端 API 返回表名列表 --> </select> <input type="text" name="column" placeholder="字段名" required> <button type="submit">创建索引</button> </form> <script> function createIndex(e) { e.preventDefault(); const form = e.target; const data = new FormData(form); fetch('/api/index', { method: 'POST', body: JSON.stringify({ table: data.get('table'), column: data.get('column') }) }); } </script>效果对比(10 万行测试):
查询方式 条件 耗时 全表扫描 WHERE name='Alice'~1200ms 哈希索引 WHERE id=12345~3ms 为什么只做哈希索引?
- B+ 树索引需维护树结构、分裂合并,代码量爆炸,偏离“简易”定位;
- 哈希索引完美支持
=查询,覆盖 80% 的教学场景;LIKE '%abc'或范围查询(BETWEEN)自然走全表,让学生明白索引局限性。
6. 面试与教学价值:如何把这套源码变成你的技术谈资
这套“基于 Java 与 HTML 的简易数据库系统”,最大的价值不在功能多强,而在于它是一块可触摸、可调试、可辩论的技术琥珀。我带实习生时,让他们每人 fork 一份,在上面加一个功能:有人加事务日志(WAL),有人加 MVCC 版本控制,有人把 HTML 换成 Vue 并对比性能。过程中暴露的问题,比十道八股文深刻得多。比如:
- 当他试图给
INSERT加事务,发现saveToCsv()失败后rows已修改,必须实现rollback()—— 这时才懂 ACID 的 “A”(原子性)不是一句口号; - 当他尝试用
ConcurrentHashMap替换HashMap存rows,结果select依然报ConcurrentModificationException—— 这时才明白“线程安全容器”不等于“线程安全操作”; - 当他把
indexMap从HashMap换成ConcurrentHashMap,发现computeIfAbsent在高并发下仍可能重复计算 —— 这时才去查 Javadoc,发现它不是原子操作。
所以,别把它当作业交完就删。我的习惯是:
- 保留 commit 记录:
git log --oneline能看到你从 CSV 存储 → JSON → SQLite 的演进; - 写 README.md 时,用表格对比每种存储的优劣(如“CSV:✅ 人眼可读 ❌ 无并发”);
- 在
src/test/java下写 JUnit 测试,哪怕只有 3 个用例:testInsertAndSelect,testIndexSpeed,testChineseCharset; - 面试时主动展示:不等面试官问“做过什么项目”,直接说:“我写过一个简易数据库,这里有个 bug 我花了两天才定位到……” —— 技术深度藏在 debug 过程里,不在功能列表里。
希望帮到你。
本文还有配套的精品资源,点击获取