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

深入Spring Boot生态中最核心部分 数据库交互spring-boot-starter-data-jpa和Hibernate (指南五)

我们继续深入Spring Boot生态中最核心、也是最能体现其工程化思想的部分:数据库交互。对于PHP开发者而言,你可能熟悉PDO、查询构造器(Query Builder),或者更常用的是ORM(对象关系映射)工具,如Laravel的Eloquent或Symfony的Doctrine。

Spring Boot通过spring-boot-starter-data-jpa提供了一套极其强大和优雅的持久化解决方案。让我们深入学习它,并与你熟悉的PHP ORM进行对比,你将会发现两者在哲学思想上的异同。


核心概念:分层的持久化世界

在PHP世界,Eloquent是一个典型的Active Record实现。你的User模型类继承自Eloquent\Model,这个模型对象本身就“知道”如何与数据库交互(User::find(1), $user->save())。模型、业务逻辑和持久化逻辑紧密地耦合在一起,这非常便捷,开发效率极高。

Java的持久化世界则更加分层和标准化,主要涉及以下几个概念:

  1. JDBC (Java Database Connectivity): 这是最底层的Java数据库API,提供了一套标准的接口来连接和执行SQL。

    • PHP类比: 类似PHP的PDO扩展,提供了基础的、与具体数据库无关的数据库操作能力。
  2. JPA (Jakarta Persistence API): JPA不是一个工具,而是一套官方规范和标准。它定义了Java对象如何映射到数据库表(ORM的规则),以及如何对这些对象进行增删改查。它只提供接口(如EntityManager),不提供实现。

    • PHP类比: 想象一下PHP社区共同制定了一个“ORM标准接口”(PSR-ORM),规定了模型该如何定义、数据该如何存取。JPA就是这样的一个角色。
  3. Hibernate: Hibernate是JPA规范最著名、最强大的实现者。它是一个具体的ORM框架,负责解析你的映射配置,生成SQL,管理事务和缓存等脏活累活。

    • PHP类比: Hibernate的角色类似于Doctrine,是Symfony的默认ORM,也是一个功能完备、实现了数据映射器模式(Data Mapper)的ORM引擎。
  4. Spring Data JPA: 这是Spring生态对JPA的终极封装。它本身不做ORM,而是像一个“智能粘合剂”,将Hibernate和JPA的使用体验提升到了一个全新的、不可思议的便捷高度。它提供了Repository(仓库)模式的抽象,让你几乎不用写任何实现代码就能完成绝大部分数据库操作。

    • PHP类比: 想象一个Laravel包,它能让你只定义一个接口,就自动获得了所有Eloquent的查询功能,并且还增加了更多魔法。Spring Data JPA就是这样的存在。

总结一下:你使用Spring Data JPA的接口进行编程,Spring Data JPA在底层调用JPA标准接口,而Hibernate则在幕后作为JPA的具体实现来完成真正的工作。


第一章:项目设置与数据库连接

1.1 添加依赖

首先,我们需要在pom.xml中添加两个核心依赖:

  1. spring-boot-starter-data-jpa: 这个“启动器”会自动引入所有需要的库,包括JPA API、Hibernate以及Spring Data JPA本身。
  2. 数据库驱动: 你需要为你选择的数据库添加具体的JDBC驱动。
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency></dependencies>

PHP类比: 这相当于在composer.jsonrequirelaravel/framework(它自带Eloquent),并确保php.ini中启用了pdo_mysql扩展。

1.2 配置数据库连接

接下来,在application.yml中配置数据库连接信息。

# application.yml
spring:# --- Datasource Configuration ---datasource:url: jdbc:mysql://127.0.0.1:3306/my_database?useSSL=false&serverTimezone=UTCusername: rootpassword: your_passworddriver-class-name: com.mysql.cj.jdbc.Driver # 通常可以省略,Spring Boot能自动检测# --- JPA & Hibernate Configuration ---jpa:hibernate:# ddl-auto: VERY important for development!# none: (Default for MySQL) Do nothing. Recommended for production.# validate: Check if the schema matches the entities.# update: Automatically add columns, tables etc. DANGEROUS in production.# create-drop: Create schema on startup, drop it on shutdown. Good for tests.ddl-auto: updateshow-sql: true # Log generated SQL to the console, great for debuggingproperties:hibernate:# Format the logged SQL to be more readableformat_sql: true

PHP类比: spring.datasource部分完全等同于你在.env文件中设置的DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD

spring.jpa.hibernate.ddl-auto的深入理解:
这个配置非常方便,但也极其危险。update模式会在应用启动时,检查你的Java实体类(Entity)和数据库表结构,如果发现不一致,它会尝试自动修改数据库表(如添加列)。

  • 开发时: 设置为update可以让你快速迭代,不用每次修改实体类都手动写数据库变更语句。
  • 生产时: 必须设置为nonevalidate 绝对不能让程序自动修改生产数据库的结构。生产环境的数据库结构变更应该通过专门的迁移工具(如Flyway或Liquibase)来严格管理。
  • 与PHP对比: ddl-auto: update有点像一个简化的、自动运行的php artisan migrate。但它远没有Laravel Migration系统强大和安全,因为它没有版本控制,也无法处理复杂的数据迁移。

第二章:实体(Entity)- 你的模型类

在JPA中,映射到数据库表的Java对象被称为实体(Entity)

PHP Eloquent 示例 (app/Models/User.php):

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;class User extends Authenticatable
{use HasFactory;protected $table = 'users';protected $fillable = ['name', 'email', 'password'];
}

Java JPA 实体示例 (src/.../entity/User.java):

package com.example.demoapp.entity;import jakarta.persistence.*;
import java.time.LocalDateTime;@Entity // 1. 声明这是一个JPA实体类
@Table(name = "users") // 2. 映射到数据库中的'users'表
public class User {@Id // 3. 标记这是主键@GeneratedValue(strategy = GenerationType.IDENTITY) // 4. 主键生成策略(自增)private Long id;@Column(nullable = false, length = 100) // 5. 映射到列,并添加约束private String name;@Column(nullable = false, unique = true)private String email;private String password; // 简单映射,列名与字段名相同@Column(name = "created_at", updatable = false)private LocalDateTime createdAt;@PrePersist // 6. 在持久化(INSERT)之前自动调用protected void onCreate() {createdAt = LocalDateTime.now();}// --- IMPORTANT: JPA requires a no-argument constructor and getters/setters ---public User() {}// Getters and Setters for all fields...// (可以使用Lombok的 @Getter, @Setter, @NoArgsConstructor 来简化)
}

注解解析与对比:

  1. @Entity: 告诉JPA:“这个类是一个实体,请为我管理它”。等同于extends Model
  2. @Table(name = "users"): 指定表名。等同于protected $table = 'users'。如果省略,默认使用类名(user)。
  3. @Id: 标记主键字段。等同于Eloquent中$primaryKey属性的约定
  4. @GeneratedValue: 配置主键的生成方式。IDENTITY对应MySQL的AUTO_INCREMENT
  5. @Column: 详细配置字段与列的映射。你可以定义nullable, unique, length等。这部分功能更像是Laravel的Migration文件中的Schema定义,如$table->string('name', 100)->nullable(false);。JPA将模型属性和部分表结构定义融合在了一起。
  6. @PrePersist: 这是一个生命周期回调注解。@PrePersist注解的方法会在新实体被保存到数据库(执行INSERT)之前调用。等同于Eloquent中的模型事件(Model Events),如boot()方法中定义的static::creating(function ($user) { ... })

第三章:仓库(Repository)- 数据访问的革命

这是Spring Data JPA与Eloquent(Active Record模式)思想上最大的分歧点,也是其魅力的核心。

Eloquent中,模型自己负责数据操作。Spring Data JPA则遵循仓库模式(Repository Pattern),将数据访问的职责从实体中分离出来,交给一个专门的接口——Repository。

操作步骤:

  1. 创建一个接口 UserRepository:
    entity包旁边创建一个repository包,然后创建一个接口(Interface),而不是类。
    package com.example.demoapp.repository;import com.example.demoapp.entity.User;
    import org.springframework.data.jpa.repository.JpaRepository;
    import java.util.Optional;// 1. 定义一个接口,并继承 JpaRepository
    public interface UserRepository extends JpaRepository<User, Long> {// 2. 仅需定义方法签名,无需任何实现!Optional<User> findByEmail(String email);
    }
    

发生了什么?

  1. extends JpaRepository<User, Long>: 这是魔法的核心。通过继承JpaRepository,Spring Data JPA在应用启动时,会自动为你生成这个接口的实现类,并将其注册为一个Bean。这个实现类已经包含了全套的CRUD方法!

    • User: 泛型参数,指定这个Repository是为User实体服务的。
    • Long: 泛型参数,指定User实体的主键类型是Long
  2. 你获得的“免费”方法 (部分):

    • save(User user): 创建或更新用户。等同于$user->save()
    • findById(Long id): 按ID查找用户。等同于User::find($id)
    • findAll(): 获取所有用户。等同于User::all()
    • deleteById(Long id): 按ID删除用户。等同于User::destroy($id)
    • count(): 统计用户数。等同于User::count()
    • …还有分页查询、批量操作等大量方法。
  3. 查询衍生(Query Derivation): 这是第二个魔法。你在接口中定义了一个findByEmail(String email)方法。你没有写任何实现代码,但Spring Data JPA会解析这个方法名,并自动为你生成并执行如下的JPQL(JPA查询语言,类似于SQL)查询:"select u from User u where u.email = ?1"

    • 方法命名规则: findBy{字段名}countBy{字段名}deleteBy{字段名}… 你还可以组合And, Or, GreaterThan, Like, OrderBy等关键字。例如:List<User> findByNameContainingAndStatusOrderByCreatedAtDesc(String keyword, String status);
    • PHP类比: 这就像是Eloquent的魔术where方法User::whereEmail($email)->first())的超级进化版。你将查询的意图直接固化在了一个个强类型的方法签名中,而不是在代码里拼接字符串。

第四章:在服务层中使用Repository

现在,我们将Repository注入到服务层(Service Layer)来执行业务逻辑。

package com.example.demoapp.service;import com.example.demoapp.entity.User;
import com.example.demoapp.repository.UserRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;@Service // 标记为服务层组件
public class UserService {private final UserRepository userRepository;// 通过构造函数注入UserRepositorypublic UserService(UserRepository userRepository) {this.userRepository = userRepository;}public User createUser(String name, String email, String password) {User newUser = new User();newUser.setName(name);newUser.setEmail(email);newUser.setPassword(password); // (实际项目中需要加密)return userRepository.save(newUser); // 调用save方法持久化}public Optional<User> getUserByEmail(String email) {// 直接调用我们刚才在接口中定义的魔法方法return userRepository.findByEmail(email);}public List<User> getAllUsers() {return userRepository.findAll(); // 获取所有用户}
}

与PHP对比:
在Laravel中,你可能会在Controller或Service中直接使用Eloquent模型:User::create([...])。在Spring Boot中,你注入对应的Repository,然后调用Repository的方法:userRepository.save(user)

这种**关注点分离(Separation of Concerns)**是Java企业级应用的核心思想:

  • Entity: 只负责描述数据结构和映射关系。
  • Repository: 只负责数据的存取。
  • Service: 只负责实现业务逻辑,协调Entity和Repository。

总结对比

特性/概念PHP (Eloquent)Java (Spring Data JPA + Hibernate)心态转变
设计模式Active Record (模型自带存取方法)Repository + Data Mapper (存取逻辑分离到仓库)从模型万能论到职责分离,代码更清晰,可测试性更强。
模型/实体class User extends Model {} @Entity class User {}概念类似,但JPA实体通过注解承载了更多元数据。
数据访问User::find(1), $user->save() (静态/实例方法)注入UserRepository, 调用repo.findById(1), repo.save(user)从静态调用模型到注入专门的数据访问对象。
查询构建查询构造器 User::where(...) / Eloquent查询查询衍生 repo.findByName(...) / JPQL / Criteria API从链式调用构建查询到通过方法命名或专用查询语言定义查询。
数据库迁移Migration系统 (php artisan migrate)无内置迁移系统ddl-auto仅供开发。生产需用Flyway/Liquibase必须引入专业的数据库版本控制工具,这是Java工程化的体现。
核心优势开发效率极高,上手快,简单直观类型安全,高度解耦,标准化,IDE支持极佳,易于测试和维护从追求极致的开发速度到构建一个长期、稳定、可维护的系统。

虽然Spring Data JPA的入门曲线比Eloquent要陡峭一些,因为它引入了更多的分层和概念。但一旦你掌握了Entity、Repository和Service的协作模式,你将能构建出结构极其清晰、类型安全、易于测试和扩展的大型应用。这种由框架强制执行的“最佳实践”正是Java生态的魅力所在。

前面的教程我们聚焦于如何使用spring-boot-starter-data-jpa,而这个使用体验主要是由Spring Data JPA塑造的。在这个过程中,Hibernate就像一台性能强劲但藏在幕后的发动机,我们并没有直接操作它,但它完成了所有最艰苦的工作。

现在,我们就把发动机盖打开,深入探究一下Hibernate本身到底是什么,它在整个体系中扮演了什么角色,以及它提供了哪些JPA规范之外的强大功能


重新理解我们的技术栈:一个精准的类比

为了清晰地理解JPA和Hibernate的关系,我们可以用一个接口和实现的类比:

  • JPA (Jakarta Persistence API): 就像是USB接口标准。它是一份公开的、行业公认的规范。它定义了USB接口应该是什么形状、有几根针脚、每根针脚的功能是什么。但这份规范本身并不能帮你存储数据,它只是一套标准。

  • Hibernate: 就像是一个高性能的移动硬盘(例如三星或西部数据品牌)。这个硬盘遵循并实现了USB接口标准,所以它可以插在任何有USB口的电脑上使用。但除了实现标准接口外,这个硬盘内部还有自己独家的、先进的技术,比如特殊的缓存算法、磨损均衡技术等,这些是它作为一款优秀产品的核心竞争力。

  • Spring Data JPA: 就像是操作系统中那个极其智能、好用的“文件管理器”或“驱动程序”。当你把移动硬盘插上电脑,你通常不会直接用二进制命令去操作硬盘,而是通过这个文件管理器进行“复制”、“粘贴”等简单操作。文件管理器利用了USB标准与硬盘通信,让你的使用体验变得极其简单。它能与任何实现了USB标准的硬盘工作,但它与像Hibernate这样主流、强大的硬盘配合得最好。

结论:你日常编程接触的是Spring Data JPA(文件管理器),它让你用最简单的方式下达指令。Spring Data JPA将你的指令翻译成JPA标准(USB标准)的调用。而Hibernate(移动硬盘)作为JPA标准的实现者,在底层接收这些标准调用,并用自己强大的内部引擎完成实际的数据读写、事务管理和性能优化。


Hibernate的核心职责:它在幕后做了什么?

当你调用userRepository.save(user)时,背后发生了什么?Hibernate主要承担了以下几个核心职责:

1. 终极翻译官:从对象到SQL (SQL Generation)

这是Hibernate最基本也最重要的工作。它负责在你的Java对象世界和关系型数据库的SQL世界之间进行双向翻译。

  • 对象 -> SQL: 当你调用save(user)时,Hibernate会读取User实体类的所有@注解(@Entity, @Table, @Column等),然后动态地生成一条对应数据库方言(Dialect)的INSERT SQL语句。如果调用findById(1L),它会生成SELECT * FROM users WHERE id = ?
  • SQL -> 对象: 当查询到数据后,Hibernate会读取结果集(ResultSet),并根据映射关系,将一行行的数据“水合”(Hydrate)成一个个的User对象实例。

关键特性:数据库方言 (Dialect)
Hibernate非常智能,它知道不同数据库的SQL语法有细微差别。例如,MySQL的分页用LIMIT,Oracle用ROWNUM;MySQL的主键自增是AUTO_INCREMENT,PostgreSQL是SERIAL。你只需要在application.yml中配置好spring.datasource.url,Hibernate就会自动选择合适的方言来生成最适配当前数据库的SQL。

PHP类比:这部分工作类似于Eloquent或Doctrine的查询构造器(Query Builder)。当你链式调用->where()->orderBy()->get()时,ORM在底层为你拼接SQL字符串。Hibernate以一种更全面、更自动化的方式完成了这项工作。

2. 状态管理器:持久化上下文与生命周期 (Persistence Context)

这是理解ORM精髓的关键,也是Hibernate强大的地方。Hibernate内部维护了一个叫做**持久化上下文(Persistence Context)**的概念,在JPA规范中它被称为EntityManager,在Hibernate的早期API中它被称为Session

你可以把它想象成一个**“实体暂存区”或“工作单元”**,它与一个数据库事务绑定。所有在这个事务中被加载或保存的实体,都会被放入这个上下文中进行管理。

实体在Hibernate中有四种状态:

  1. 瞬时态 (Transient): 一个普通的new User()对象,与持久化上下文无任何关联。
  2. 持久态 (Persistent): 该对象在持久化上下文中,它的任何变更都会被Hibernate追踪。通过find()save()后的实体就处于这个状态。
  3. 游离态 (Detached): 该对象曾经被管理,但由于持久化上下文被关闭(如事务结束),它现在脱离了管理。
  4. 删除态 (Removed): 准备从数据库中删除的对象。

核心特性:自动脏检查 (Automatic Dirty Checking)
这是持久化上下文带来的最大便利。

@Transactional
public void updateUserEmail(Long userId, String newEmail) {// 1. findById使user对象进入“持久态”,被Hibernate纳入管理User user = userRepository.findById(userId).orElseThrow(); // 2. 你只修改了Java对象的状态,没有调用saveuser.setEmail(newEmail); // 3. 当这个@Transactional方法结束,事务准备提交时...// Hibernate会自动检查其管理的所有“持久态”对象。// 它发现user对象的email字段与刚从数据库加载时的快照不同(变“脏”了)。// 于是,Hibernate会自动生成一条UPDATE语句并执行,将变更同步到数据库。
}

不需要手动调用userRepository.save(user)来更新!这就是“脏检查”的威力。

PHP类比:Eloquent也有类似机制。当你执行$user = User::find(1); $user->email = '...'; $user->save();时,save()方法内部会检查哪些属性被修改过(isDirty),然后只更新修改过的字段。Hibernate的强大之处在于,结合Spring的@Transactional,这个save()的调用过程在很多更新场景下都被隐式地、自动地完成了

3. 性能加速器:多级缓存机制 (Caching)

为了减少对数据库的昂贵访问,Hibernate内置了一套复杂的缓存系统。

  • 一级缓存 (L1 Cache / Session Cache):

    • 这就是我们上面提到的持久化上下文。它的生命周期与Session或事务相同。
    • 作用: 在同一个事务内,如果你多次通过ID请求同一个对象,只有第一次会查询数据库。后续的请求,Hibernate会直接从一级缓存(一个内部Map)中返回这个对象,避免了重复的SQL查询。
    • 这是默认开启且无法关闭的
    @Transactional
    public void L1CacheExample(Long userId) {User user1 = userRepository.findById(userId).get(); // 发送SELECT SQLUser user2 = userRepository.findById(userId).get(); // 不会发SQL,直接从L1缓存返回System.out.println(user1 == user2); // 输出 true, 两个引用指向同一个内存中的对象
    }
    
  • 二级缓存 (L2 Cache / Global Cache):

    • 这是一个跨事务、跨Session的全局缓存,它的生命周期和整个应用程序一样长。
    • 作用: 用于缓存那些很少被修改但经常被读取的数据,例如国家列表、系统配置、用户角色等。当一个事务请求某个实体时,Hibernate会先检查L2缓存,如果命中,就无需访问数据库。
    • 默认关闭,需要手动配置。你需要引入一个缓存提供商(如EhCache, Hazelcast, Infinispan),然后在application.yml中开启L2缓存,并在需要缓存的实体类上添加@Cacheable注解。

PHP类比

  • 一级缓存:类似于在一个请求的生命周期内,一些设计精良的PHP ORM可能会做的内部对象缓存。
  • 二级缓存:完全等同于你在PHP项目中引入Redis或Memcached,并使用Cache::remember('user:'.$id, ...)这样的模式来手动缓存模型数据。Hibernate的优势在于,它将这个缓存过程与ORM深度集成,对开发者更透明,配置好之后几乎是自动的。
4. 高级查询武器:HQL/JPQL 和 Criteria API

虽然Spring Data JPA的查询衍生(findByEmail)能解决80%的问题,但对于复杂的查询(例如多表JOIN、子查询、聚合函数等),我们需要更强大的工具。Hibernate提供了两种方式:

  • HQL (Hibernate Query Language) / JPQL (Jakarta Persistence Query Language):

    • 这是一种面向对象的查询语言。你查询的不是数据库表和列,而是Java实体和属性
    • 语法和SQL非常相似,但更具可移植性,因为最终由Hibernate把它翻译成特定数据库方言的SQL。
    • 在Spring Data JPA中,通常通过@Query注解在Repository方法上使用。
    // UserRepository.java
    @Query("SELECT u FROM User u WHERE u.status = :status AND u.createdAt > :date")
    List<User> findActiveUsersSince(@Param("status") String status, @Param("date") LocalDateTime date);
    

    注意,我们写的是FROM User u(类名和别名),而不是FROM users(表名)。

  • Criteria API:

    • 这是一种纯Java的、类型安全的、程序化的方式来构建动态查询。你可以通过调用一系列方法来构建一个查询对象,完全避免了拼接字符串。
    • 代码更冗长,但类型安全,可以在编译期发现错误。

PHP类比:

  • HQL/JPQL:非常类似于Doctrine的DQL。对于Laravel开发者来说,它像是查询构造器的一种更结构化、更安全的替代品,特别是当你需要构建的查询逻辑非常复杂时。
  • Criteria API:类似于你在PHP中以纯程序化方式构建查询对象(一些高级查询构造器库提供了这种能力),优点是所有东西都是代码,易于重构和进行条件判断。

总结:JPA, Hibernate, Spring Data JPA的角色分工

组件角色PHP类比核心价值
JPA标准/规范 (一套接口)类似PSR (PHP Standard Recommendation)可移植性:理论上可以随时将Hibernate替换为其他JPA实现(如EclipseLink)而无需修改业务代码。
HibernateJPA的实现 (一个强大的引擎)Doctrine (一个具体的、强大的ORM实现)功能强大:负责SQL生成、事务管理、生命周期、多级缓存、HQL等所有底层重活。
Spring Data JPAJPA的简化封装 (一个便捷的工具)像是Eloquent的便捷性 + 仓库模式的优雅开发效率:通过Repository接口和查询衍生,极大简化了数据访问层的代码,让你几乎不用写实现。

作为一名从PHP转过来的开发者,你可以这样理解:
你失去了Eloquent那种模型无所不能(Active Record)的极致便捷,但你得到的是一个更加分层、解耦、健壮和高性能的持久化架构。你主要与Spring Data JPA的Repository打交道,享受它带来的便利,同时心中要清楚,是强大的Hibernate引擎在底层为你保驾护航,而JPA标准则保证了这套架构的开放性和通用性。

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

相关文章:

  • 如何使用Python实现UDP广播
  • ThinkPHP 入门:快速构建 PHP Web 应用的强大框架
  • 系统架构 从_WHAT_走向_HOW_的锻造之路
  • UNIX下C语言编程与实践6-Make 工具与 Makefile 编写:从基础语法到复杂项目构建实战
  • 事业单位网站模板网站开发png图标素材
  • 电子商务网站建设外包服务p2p理财网站开发框架
  • Gateway 集成 JWT 身份认证:微服务统一认证的实战指南
  • C语言数据类型与变量详解
  • 【开题答辩全过程】以 php厦门旅游信息网站管理系统开题为例,包含答辩的问题和答案
  • 《重构工业运维链路:三大AI工具让设备故障“秒定位、少误判”》
  • 大模型的第一性原理考量:基于物理本质与数学基础的范式重构
  • Ubuntu 系统安装 Prometheus+Grafana(附shell脚本一键部署↓)
  • Airbnb内部核心键值存储系统 Mussel 已完成从 v1 到 v2 的重构升级
  • 漳州做网站配博大钱少awordpress国内网站吗
  • 在用户调研中应用卡尔曼滤波:用动态思维重构认知更新
  • [免费]基于Python的在线音乐网站系统(后端Django)【论文+源码+SQL脚本】
  • 网站建设后期出现的问题手机网站开发前台架构
  • 首饰设计网站大全如何选择昆明网站建设
  • MapDistill:通过相机 - 激光雷达融合模型蒸馏提升高效基于相机的高清地图构建性能
  • 安卓开发---在适配器中使用监听器
  • 【COT】PromptCoT 2.0少样本训练 CoT
  • 鸿蒙应用开发和安卓应用开发的区别
  • UNIX下C语言编程与实践12-lint 工具使用指南:C 语言源代码语法与逻辑错误检查实战
  • UNIX下C语言编程与实践5-C 语言编译器 cc(gcc/xlc)核心参数解析:-I、-L、-D 的使用场景与实例
  • 机器视觉的双相机对位模切应用
  • 高档网站设计wordpress好用的商城主题
  • 植物大战僵尸融合版下载安装教程【PC/安卓/iOS 完整攻略 + 常见问题解决】
  • 厦门做网站价格想学网站建设与设计的书籍
  • 【TCSVT→Neurocomputing→ASOC投稿】记一次旷日持久的投稿经历
  • namespace 扩展