当前位置: 首页 > news >正文

[手写系列]Go手写db — — 第五版(实现数据库操作模块)

[手写系列]Go手写db — — 第五版(实现数据库操作模块)

第一版文章:[手写系列]Go手写db — — 完整教程_go手写数据库-CSDN博客
第二版文章:[手写系列]Go手写db — — 第二版-CSDN博客
第三版文章:[手写系列]Go手写db — — 第三版(实现分组、排序、聚合函数等)-CSDN博客
第四版文章:[手写系列]Go手写db — — 第四版(实现事务、网络模块)

  • 整体项目Github地址:https://github.com/ziyifast/ZiyiDB
  • 请大家多多支持,也欢迎大家star⭐️和共同维护这个项目~

本文主要介绍如何在 ZiyiDB 第四版的基础上,实现数据库层面的操作,包括创建、删除、切换数据库以及数据库与表的关联管理等,通过这些功能,将使得ZiyiDB成为一个更完整的数据库系统。

一、功能列表

  1. 新增对数据库的创建(CREATE DATABASE)支持
  2. 新增对数据库的删除(DROP DATABASE)支持
  3. 新增对数据库列表的展示(SHOW DATABASES)支持
  4. 新增对表列表的展示(SHOW TABELS)支持
  5. 新增对数据库切换(USE DATABASE)支持
  6. 实现数据库与表的关联管理。存储引擎操作数据库,然后由数据库结构体间接操作表

二、实现细节

功能点一:实现数据库的创建、删除

实现思路

  1. internal/lexer/token.go新增DATABASE关键字,因为drop和create关键字之前已经有了
    在这里插入图片描述
  2. internal/lexer/lexer.go中的lookupIdentifier方法新增一个case返回,用于将用户输入的字符转换为TokenType
    在这里插入图片描述
  3. internal/ast/ast.go语法树中新增create database和drop database语法树结构
    在这里插入图片描述
  4. internal/parser/parser.go语法解析器中的create和drop case中新增对database的处理
    在这里插入图片描述
    在这里插入图片描述
    然后分别实现parseCreateDatabaseStatement、parseDropDatabaseStatement方法,方便后续构建抽象语法树
    在这里插入图片描述
    在这里插入图片描述
  5. internal/storage/memory.go存储引擎
  • 新增数据库结构体的定义,同时存储引擎直接管理数据库,数据表交给Database结构体管理
    在这里插入图片描述
  • 实现底层对数据库创建、删除的方法
    在这里插入图片描述
  1. cmd/main.go中executor方法新增case处理对用户对数据库创建、删除的操作

PS:network/server.go网络服务端修改同理,这里不做赘述

在这里插入图片描述

代码实现

1. 词法分析器调整

internal/lexer/token.go:

// internal/lexer/token.go
package lexer// TokenType 表示词法单元类型
type TokenType stringconst (...DATABASE  TokenType = "DATABASE"...
)// Token 词法单元
// Type:标记的类型(如 SELECT、IDENT 等)
// Literal:标记的实际值(如具体的列名、数字等)
type Token struct {Type    TokenType // 标记类型Literal string    // 标记的实际值
}

internal/lexer/lexer.go:

// internal/lexer/lexer.go
package lexerimport ("bufio""bytes""io""strings""time""unicode"
)...// lookupIdentifier 查找标识符类型
// 将标识符转换为对应的标记类型
// 识别 SQL 关键字
func (l *Lexer) lookupIdentifier(ident string) TokenType {switch strings.ToUpper(ident) {...case "DATABASE":return DATABASE...default:return IDENT}
}...
2. 抽象语法树调整

internal/ast/ast.go:

...// CreateDatabaseStatement 表示CREATE DATABASE语句type CreateDatabaseStatement struct {Token lexer.TokenName  string}func (cds *CreateDatabaseStatement) statementNode()       {}func (cds *CreateDatabaseStatement) TokenLiteral() string { return cds.Token.Literal }// DropDatabaseStatementtype DropDatabaseStatement struct {Token lexer.TokenName  string}func (dds *DropDatabaseStatement) statementNode()       {}func (dds *DropDatabaseStatement) TokenLiteral() string { return dds.Token.Literal }...
3. 语法解析器调整
// parseStatement 解析语句
// 根据当前标记类型选择相应的解析方法
func (p *Parser) parseStatement() (ast.Statement, error) {// 跳过注释for p.curToken.Type == lexer.COMMENT {p.nextToken()}switch p.curToken.Type {//需要区分是创建表还是创建数据库case lexer.CREATE:if p.peekTokenIs(lexer.TABLE) {return p.parseCreateTableStatement()} else if p.peekTokenIs(lexer.DATABASE) {return p.parseCreateDatabaseStatement()}return nil, fmt.Errorf("expected TABLE or DATABASE after CREATE")...case lexer.DROP:if p.peekTokenIs(lexer.TABLE) {return p.parseDropTableStatement()} else if p.peekTokenIs(lexer.DATABASE) {return p.parseDropDatabaseStatement()}return nil, fmt.Errorf("expected TABLE or DATABASE after DROP")case lexer.SEMI:return nil, nildefault:return nil, fmt.Errorf("You have an error in your SQL syntax; check the manual that corresponds to your db server version for the right syntax to use near '%s'", p.curToken.Type)}
}// parseDropDatabaseStatement 解析DROP DATABASE语句
func (p *Parser) parseDropDatabaseStatement() (*ast.DropDatabaseStatement, error) {stmt := &ast.DropDatabaseStatement{Token: p.curToken}if !p.expectPeek(lexer.DATABASE) {return nil, fmt.Errorf("expected DATABASE keyword")}if !p.expectPeek(lexer.IDENT) {return nil, fmt.Errorf("expected database name")}stmt.Name = p.curToken.Literalreturn stmt, nil
}// parseCreateDatabaseStatement 解析CREATE DATABASE语句
func (p *Parser) parseCreateDatabaseStatement() (*ast.CreateDatabaseStatement, error) {stmt := &ast.CreateDatabaseStatement{Token: p.curToken}if !p.expectPeek(lexer.DATABASE) {return nil, fmt.Errorf("expected DATABASE keyword")}if !p.expectPeek(lexer.IDENT) {return nil, fmt.Errorf("expected database name")}stmt.Name = p.curToken.Literalreturn stmt, nil
}
4. 存储引擎实现操作
// Database 表示数据库
type Database struct {Name   stringTables map[string]*Tablemu     sync.RWMutex
}// MemoryBackend 内存存储引擎,管理所有数据库
type MemoryBackend struct {Databases map[string]*DatabasetxnMgr    *TransactionManagerMu        sync.RWMutex
}// Table 数据表,包含列定义、数据行和索引
type Table struct {Name     stringColumns  []ast.ColumnDefinitionRows     [][]VersionedCell // 保持为 VersionedCellIndexes  map[string]*IndexRowLocks map[int]*sync.RWMutex // 行级锁mu       sync.RWMutex
}...// CreateDatabase 创建数据库
func (b *MemoryBackend) CreateDatabase(stmt *ast.CreateDatabaseStatement) error {b.Mu.Lock()defer b.Mu.Unlock()if _, exists := b.Databases[stmt.Name]; exists {return fmt.Errorf("database '%s' already exists", stmt.Name)}b.Databases[stmt.Name] = &Database{Name:   stmt.Name,Tables: make(map[string]*Table),}return nil
}// DropDatabase 删除数据库
func (b *MemoryBackend) DropDatabase(stmt *ast.DropDatabaseStatement) error {b.Mu.Lock()defer b.Mu.Unlock()if _, exists := b.Databases[stmt.Name]; !exists {return fmt.Errorf("database '%s' does not exist", stmt.Name)}delete(b.Databases, stmt.Name)return nil
}
5. 程序入口处调整

需要调整两个地方,一个是本地命令行版(cmd/main.go),一个是网络版(network/server.go)

  • cmd/main.go:

func executor(t string) {// 分割多个SQL语句(用分号分隔)statements := strings.Split(t, ";")for _, stmt := range statements {...// 创建词法分析器l := lexer.NewLexer(strings.NewReader(stmt))// 创建语法分析器p := parser.NewParser(l)// 解析SQL语句parsedStmt, err := p.ParseProgram()if err != nil {fmt.Printf("Parse error: %v\n", err)continue}// 执行SQL语句for _, statement := range parsedStmt.Statements {if currentDatabase == "" {// 检查是否是非数据库操作语句_, isCreateDB := statement.(*ast.CreateDatabaseStatement)_, isDropDB := statement.(*ast.DropDatabaseStatement)// 如果不是允许的语句类型,则提示需要选择数据库if !isCreateDB && !isDropDB {fmt.Println("No database selected. Use 'USE database_name' to select a database.")continue}}switch s := statement.(type) {case *ast.CreateDatabaseStatement:if err := backend.CreateDatabase(s); err != nil {fmt.Printf("Error: %v\n", err)} else {fmt.Println("Database created successfully")}case *ast.DropDatabaseStatement:if err := backend.DropDatabase(s); err != nil {fmt.Printf("Error: %v\n", err)} else {fmt.Println("Database dropped successfully")}...default:fmt.Printf("Unsupported statement type: %T\n", s)}}}
}
  • network/server.go:
// network/server.go
package networkimport ("bufio""fmt""net""strings""sync""ziyi.db.com/internal/ast""ziyi.db.com/internal/lexer""ziyi.db.com/internal/parser""ziyi.db.com/internal/storage"
)const DefaultPort = "3118"...func (s *Server) executeCommand(conn net.Conn, command string) string {...var result stringfor _, statement := range parsedStmt.Statements {//检查是否选择了数据库if connCtx.GetDBName() == "" {// 检查是否是非数据库操作语句_, isCreateDB := statement.(*ast.CreateDatabaseStatement)_, isDropDB := statement.(*ast.DropDatabaseStatement)// 如果不是允许的语句类型,则提示需要选择数据库if !isCreateDB && !isDropDB {result += "No database selected. Use 'USE database_name' to select a database."continue}}switch stmt := statement.(type) {case *ast.CreateDatabaseStatement:if err := s.backend.CreateDatabase(stmt); err != nil {result += fmt.Sprintf("Error: %v\n", err)} else {result += "Database created successfully\n"}case *ast.DropDatabaseStatement:if err := s.backend.DropDatabase(stmt); err != nil {result += fmt.Sprintf("Error: %v\n", err)} else {result += "Database dropped successfully\n"}case *ast.DropTableStatement:if err := s.backend.DropTable(connCtx.db, stmt); err != nil {result += fmt.Sprintf("Error: %v\n", err)} else {result += "Table dropped successfully\n"}default:result += fmt.Sprintf("Unsupported statement type: %T\n", stmt)}}return strings.TrimSpace(result)
}

测试

测试命令:

-- 创建test数据库
create database test;
-- 删除test数据库
drop database test;

效果:
在这里插入图片描述

功能点二:use选择数据库

实现思路

  1. 新增internal/context/context.go文件,定义DBContext接口
    在这里插入图片描述

  2. internal/lexer/token.go新增USE关键字:
    在这里插入图片描述

  3. internal/lexer/lexer.go中lookupIdentifier新增case:
    在这里插入图片描述

  4. 抽象语法树internal/ast/ast.go新增UseDatabaseStatement
    在这里插入图片描述

  5. 语法解析器internal/parser/parser.go的parseStatement新增case,并实现对应case逻辑
    在这里插入图片描述
    在这里插入图片描述

  6. 存储引擎实现UseDatabase选择数据库逻辑
    在这里插入图片描述

  7. 网络服务端、程序入口调用UseDatabase方法
    在这里插入图片描述

代码实现

1. 词法分析器调整
  • internal/lexer/token.go:
const(USE       TokenType = "USE"...
)
...
  • internal/lexer/lexer.go:
...
// lookupIdentifier 查找标识符类型
// 将标识符转换为对应的标记类型
// 识别 SQL 关键字
func (l *Lexer) lookupIdentifier(ident string) TokenType {switch strings.ToUpper(ident) {...case "USE":return USEdefault:return IDENT}
}
2. 抽象语法树调整

internal/ast/ast.go:

...
type UseDatabaseStatement struct {Token lexer.TokenName  string
}func (uds *UseDatabaseStatement) statementNode()       {}
func (uds *UseDatabaseStatement) TokenLiteral() string { return uds.Token.Literal }
3. 语法解析器调整

internal/parser/parser.go:


// parseStatement 解析语句
// 根据当前标记类型选择相应的解析方法
func (p *Parser) parseStatement() (ast.Statement, error) {// 跳过注释for p.curToken.Type == lexer.COMMENT {p.nextToken()}switch p.curToken.Type {...case lexer.USE:return p.parseUseDatabaseStatement()...default:return nil, fmt.Errorf("You have an error in your SQL syntax; check the manual that corresponds to your db server version for the right syntax to use near '%s'", p.curToken.Type)}
}func (p *Parser) parseUseDatabaseStatement() (*ast.UseDatabaseStatement, error) {stmt := &ast.UseDatabaseStatement{Token: p.curToken}if !p.expectPeek(lexer.IDENT) {return nil, fmt.Errorf("expected database name")}stmt.Name = p.curToken.Literalreturn stmt, nil
}...
4. 存储引擎实现操作

internal/storage/memory.go:

// UseDatabase 使用数据库
func (b *MemoryBackend) UseDatabase(stmt *ast.UseDatabaseStatement, connCtx context.DBContext) error {b.Mu.RLock()defer b.Mu.RUnlock()if _, exists := b.Databases[stmt.Name]; !exists {return fmt.Errorf("database '%s' does not exist", stmt.Name)}// 更新连接上下文中的当前数据库connCtx.SetDBName(stmt.Name)return nil
}
5. 程序入口处调整

需要调整两个地方,一个是本地命令行版(cmd/main.go),一个是网络版(network/server.go)

  • cmd/main.go:

func executor(t string) {// 分割多个SQL语句(用分号分隔)statements := strings.Split(t, ";")for _, stmt := range statements {...// 创建词法分析器l := lexer.NewLexer(strings.NewReader(stmt))// 创建语法分析器p := parser.NewParser(l)// 解析SQL语句parsedStmt, err := p.ParseProgram()if err != nil {fmt.Printf("Parse error: %v\n", err)continue}// 执行SQL语句for _, statement := range parsedStmt.Statements {if currentDatabase == "" {// 检查是否是非数据库操作语句_, isCreateDB := statement.(*ast.CreateDatabaseStatement)_, isDropDB := statement.(*ast.DropDatabaseStatement)// 如果不是允许的语句类型,则提示需要选择数据库if !isCreateDB && !isDropDB {fmt.Println("No database selected. Use 'USE database_name' to select a database.")continue}}switch s := statement.(type) {...case *ast.UseDatabaseStatement:if err := backend.UseDatabase(s, &dbContextAdapter{&currentDatabase}); err != nil {fmt.Printf("Error: %v\n", err)} else {fmt.Printf("Database changed to '%s'\n", currentDatabase)}...default:fmt.Printf("Unsupported statement type: %T\n", s)}}}
}
  • network/server.go:
// network/server.go
package networkimport ("bufio""fmt""net""strings""sync""ziyi.db.com/internal/ast""ziyi.db.com/internal/lexer""ziyi.db.com/internal/parser""ziyi.db.com/internal/storage"
)const DefaultPort = "3118"...func (s *Server) executeCommand(conn net.Conn, command string) string {...var result stringfor _, statement := range parsedStmt.Statements {...switch stmt := statement.(type) {case *ast.UseDatabaseStatement:if err := s.backend.UseDatabase(stmt, connCtx); err != nil {result += fmt.Sprintf("Error: %v\n", err)} else {result += fmt.Sprintf("Database changed to '%s'\n", stmt.Name)}...default:result += fmt.Sprintf("Unsupported statement type: %T\n", stmt)}}return strings.TrimSpace(result)
}

测试

测试命令:

-- 创建两个数据库
create database test;
create database test2;
-- 使用数据库test并创建表
use test;
create table users (id INT PRIMARY KEY,name text,age INT);
INSERT INTO users VALUES (1, 'Alice', 20);
select * from users;
-- 预期test2数据库中没有users表
use test2;
select * from users;

效果:
在这里插入图片描述

功能点三:实现数据库、表列表展示

实现思路

  1. internal/lexer/token.go新增关键字
    在这里插入图片描述
  2. internal/lexer/lexer.go lookupIdentifier方法新增一个case返回,用于将用户输入的字符转换为TokenType
    在这里插入图片描述
  3. internal/ast/ast.go抽象语法树新增ShowDatabasesStatement、ShowTablesStatement,方便后续语法解析器构建抽象语法树
    在这里插入图片描述
  4. internal/parser/parser.go语法解析器中实现对ShowDatabasesStatement、ShowTablesStatement抽象语法树的构建
    在这里插入图片描述
    同时在parseStatement方法中新增一个case,用于解析show相关的SQL语句
    在这里插入图片描述
  5. internal/storage/memory.go存储引擎中实现show databases、show tables,底层其实就是range遍历,然后将结果放在一个切片中
    在这里插入图片描述
  6. 程序入口处调整,新增对应的case,判断到对应Statement之后,通过存储引擎调用对应方法即可
  • cmd/main.go:
    在这里插入图片描述
  • network/server.go:
    在这里插入图片描述

代码实现

1. 词法分析器调整
  • internal/lexer/token.go:
const(...DATABASES TokenType = "DATABASES"SHOW      TokenType = "SHOW"TABLES    TokenType = "TABLES"
)
  • internal/lexer/lexer.go:
// lookupIdentifier 查找标识符类型
// 将标识符转换为对应的标记类型
// 识别 SQL 关键字
func (l *Lexer) lookupIdentifier(ident string) TokenType {switch strings.ToUpper(ident) {...case "DATABASES":return DATABASEScase "SHOW":return SHOWcase "TABLES":return TABLESdefault:return IDENT}
}
...
2. 抽象语法树调整

internal/ast/ast.go:

// ShowDatabasesStatement
type ShowDatabasesStatement struct {Token lexer.Token
}func (sds *ShowDatabasesStatement) statementNode()       {}
func (sds *ShowDatabasesStatement) TokenLiteral() string { return sds.Token.Literal }// ShowTablesStatement
type ShowTablesStatement struct {Token lexer.Token
}func (sds *ShowTablesStatement) statementNode()       {}
func (sds *ShowTablesStatement) TokenLiteral() string { return sds.Token.Literal }
...
3. 语法解析器调整

internal/parser/parser.go:

// parseStatement 解析语句
// 根据当前标记类型选择相应的解析方法
func (p *Parser) parseStatement() (ast.Statement, error) {// 跳过注释for p.curToken.Type == lexer.COMMENT {p.nextToken()}switch p.curToken.Type {//需要区分是创建表还是创建数据库case lexer.CREATE:if p.peekTokenIs(lexer.TABLE) {return p.parseCreateTableStatement()} else if p.peekTokenIs(lexer.DATABASE) {return p.parseCreateDatabaseStatement()}return nil, fmt.Errorf("expected TABLE or DATABASE after CREATE")case lexer.SHOW:if p.peekTokenIs(lexer.DATABASES) {return p.parseShowDatabasesStatement()}if p.peekTokenIs(lexer.TABLES) {return p.parseShowTablesStatement()}return nil, fmt.Errorf("expected DATABASES after SHOW")...default:return nil, fmt.Errorf("You have an error in your SQL syntax; check the manual that corresponds to your db server version for the right syntax to use near '%s'", p.curToken.Type)}
}// parseShowDatabasesStatement 解析SHOW DATABASES语句
func (p *Parser) parseShowDatabasesStatement() (*ast.ShowDatabasesStatement, error) {stmt := &ast.ShowDatabasesStatement{Token: p.curToken}if !p.expectPeek(lexer.DATABASES) {return nil, fmt.Errorf("expected DATABASES keyword")}return stmt, nil
}// parseShowTablesStatement 解析SHOW TABLES语句
func (p *Parser) parseShowTablesStatement() (*ast.ShowTablesStatement, error) {stmt := &ast.ShowTablesStatement{Token: p.curToken}if !p.expectPeek(lexer.TABLES) {return nil, fmt.Errorf("expected TABLES keyword")}return stmt, nil
}...
4. 存储引擎实现操作

internal/storage/memory.go:


// ShowDatabases 显示所有数据库
func (b *MemoryBackend) ShowDatabases() *Results {b.Mu.RLock()defer b.Mu.RUnlock()results := &Results{Columns: []ResultColumn{{Name: "Database", Type: "TEXT"},},Rows: make([][]Cell, 0),}for dbName := range b.Databases {results.Rows = append(results.Rows, []Cell{{Type: CellTypeText, TextValue: dbName},})}// 按名称排序sort.Slice(results.Rows, func(i, j int) bool {return results.Rows[i][0].TextValue < results.Rows[j][0].TextValue})return results
}// ShowTables 显示数据库中的所有表
func (b *MemoryBackend) ShowTables(connCtx context.DBContext) *Results {b.Mu.RLock()defer b.Mu.RUnlock()results := &Results{Columns: []ResultColumn{{Name: "Tables", Type: "TEXT"},},Rows: make([][]Cell, 0),}dbName := connCtx.GetDBName()if dbName == "" {return results}database := b.Databases[dbName]for tableName := range database.Tables {results.Rows = append(results.Rows, []Cell{{Type: CellTypeText, TextValue: tableName},})}// 按名称排序sort.Slice(results.Rows, func(i, j int) bool {return results.Rows[i][0].TextValue < results.Rows[j][0].TextValue})return results
}...
5. 程序入口处调整

需要调整两个地方,一个是本地命令行版(cmd/main.go),一个是网络版(network/server.go)

  • cmd/main.go:

func executor(t string) {// 分割多个SQL语句(用分号分隔)statements := strings.Split(t, ";")for _, stmt := range statements {...// 创建词法分析器l := lexer.NewLexer(strings.NewReader(stmt))// 创建语法分析器p := parser.NewParser(l)// 解析SQL语句parsedStmt, err := p.ParseProgram()if err != nil {fmt.Printf("Parse error: %v\n", err)continue}// 执行SQL语句for _, statement := range parsedStmt.Statements {if currentDatabase == "" {// 检查是否是非数据库操作语句_, isCreateDB := statement.(*ast.CreateDatabaseStatement)_, isShowDBs := statement.(*ast.ShowDatabasesStatement)_, isDropDB := statement.(*ast.DropDatabaseStatement)_, isUseDB := statement.(*ast.UseDatabaseStatement)_, isShowTables := statement.(*ast.ShowTablesStatement)// 如果不是允许的语句类型,则提示需要选择数据库if !isCreateDB && !isShowDBs && !isDropDB && !isUseDB && !isShowTables {fmt.Println("No database selected. Use 'USE database_name' to select a database.")continue}}switch s := statement.(type) {case *ast.ShowDatabasesStatement:result := backend.ShowDatabases()printResults(result)case *ast.ShowTablesStatement:result := backend.ShowTables(&dbContextAdapter{&currentDatabase})printResults(result)...default:fmt.Printf("Unsupported statement type: %T\n", s)}}}
}
  • network/server.go:
// network/server.go
package networkimport ("bufio""fmt""net""strings""sync""ziyi.db.com/internal/ast""ziyi.db.com/internal/lexer""ziyi.db.com/internal/parser""ziyi.db.com/internal/storage"
)const DefaultPort = "3118"...func (s *Server) executeCommand(conn net.Conn, command string) string {...var result stringfor _, statement := range parsedStmt.Statements {//检查是否选择了数据库if connCtx.GetDBName() == "" {// 检查是否是非数据库操作语句_, isCreateDB := statement.(*ast.CreateDatabaseStatement)_, isShowDBs := statement.(*ast.ShowDatabasesStatement)_, isDropDB := statement.(*ast.DropDatabaseStatement)_, isUseDB := statement.(*ast.UseDatabaseStatement)_, isShowTables := statement.(*ast.ShowTablesStatement)// 如果不是允许的语句类型,则提示需要选择数据库if !isCreateDB && !isShowDBs && !isDropDB && !isUseDB && !isShowTables {result += "No database selected. Use 'USE database_name' to select a database."continue}}switch stmt := statement.(type) {case *ast.ShowDatabasesStatement:results := s.backend.ShowDatabases()if err != nil {result += fmt.Sprintf("Error: %v\n", err)} else {result += formatResults(results) + "\n"}case *ast.ShowTablesStatement:results := s.backend.ShowTables(connCtx)if err != nil {result += fmt.Sprintf("Error: %v\n", err)} else {result += formatResults(results) + "\n"}...default:result += fmt.Sprintf("Unsupported statement type: %T\n", stmt)}}return strings.TrimSpace(result)
}

测试

测试命令:

-- 测试show databases:
create database test;
create database test2;
show databases;-- 测试show tables:
use test2;
create table users (id INT PRIMARY KEY,name text,age INT);
create table products (id INT PRIMARY KEY,name text,price FLOAT);
show tables;

效果:
在这里插入图片描述

http://www.dtcms.com/a/479182.html

相关文章:

  • 网站购买域名朝阳企业网站建设
  • 停车全生态系统架构
  • html电影网站模板下载工具网站推广适合哪种公司做
  • Docker 资源限制与容器管理
  • 2025直播美颜sdk洞察报告:人脸美型算法、AI修复与实时渲染创新
  • 鸿蒙:实现列表单项左滑删除
  • 【TIDE DIARY 4】Agentic Retrieval-Augmented Generation: A Survey on Agentic RAG
  • 免费 网站点击wordpress移动端禁止放大
  • s3fs 取消挂载
  • 新增模块介绍:教师代课统计系统(由社区 @记得微笑 贡献)
  • 15. shell编程之#!与/bin/bas 之间需要空格吗
  • 套模板网站网络seo优化推广
  • 聪明的上海网站帮别人做网站推广犯法吗
  • HTML 总结
  • HTML应用指南:利用POST请求获取全国塔斯汀门店位置信息
  • 鞍山 网站建设网站规划网站建设报价表
  • 云服务器怎么设置虚拟IP,云服务器能起虚拟ip吗
  • Fast DDS 默认传输机制详解:共享内存与 UDP 的智能选择
  • thinkphp开发企业网站如何做优酷网站点击赚钱
  • 供应链金融对生命科学仪器企业市场竞争力的影响研究
  • 高性能高可用设计
  • 【系统分析师】写作框架:需求分析方法及应用
  • dedecms 做网站青岛企业网站建设公司
  • wordpress网仿站建设项目前期收费查询网站
  • tcp和udp协议报文段的报文格式
  • C#异步编程:async修饰方法的返回类型说明
  • MC33PT2000控制主要功能函数代码详解三
  • C语言--数据类型
  • 需求冻结后仍频繁突破怎么办
  • 做外贸电商网站士兵突击网站怎么做