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

山东军辉建设集团有限公司 公司网站网址海南企业年报网上申报入口

山东军辉建设集团有限公司 公司网站网址,海南企业年报网上申报入口,页游平台网站,建网站需要多少钱和什么条件文章目录 用户故事数据模型选择数据库SQL与NoSQLH2、Hibernate和JPA Spring Boot Data JPA依赖关系和自动配置Spring Data JPA技术栈数据源#xff08;自动#xff09;配置 实体存储库存储User和ChallengeAttempt显示最近的ChallengeAttempt服务层控制器层用户界面 小结 文章… 文章目录 用户故事数据模型选择数据库SQL与NoSQLH2、Hibernate和JPA Spring Boot Data JPA依赖关系和自动配置Spring Data JPA技术栈数据源自动配置 实体存储库存储User和ChallengeAttempt显示最近的ChallengeAttempt服务层控制器层用户界面 小结 文章代码可以从这里下载 前端后端示例 前端代码可从这里下载前端示例 后端代码可从这里下载后端示例 前面的文章 1、一个测试驱动的Spring Boot应用程序开发 2、2 使用React构造前端应用 在敏捷开发中学会纵向划分产品需求可以在构建软件时节省大量时间。这意味着不需要等待一个完整的层完成后再进行下一层的开发而是要进行多层开发确保业务功能可正常运行。这种开发方式也有助于构建更好的产品或服务。 通过前面的前后端开发完成了用户猜数游戏可以进行试用那么就可能得到更进一步的需求如果能够访问自己的统计数据以便了解一段时间的表现系统会更好。这就有了新的用户故事。 用户故事 作为用户希望能够访问以前的尝试以便了解自己的心算能力是否随时间的推移而有所提高。 将这个用户故事映射为技术解决方案时就会发现需要存储这些尝试。这里就将介绍前面例子中缺少的部分数据层这通常意味着需要将数据存储在数据库中而且也需要将这些新需求集成到其余各层主要任务如下 存储用户尝试能够允许用户进行查询。创建新的REST API接口获取指定用户的最新尝试。创建新的服务业务逻辑来检索这些尝试。用户发送新的尝试后在Web页面上显示用户所有尝试的历史记录。 数据模型 前面已经创建了ChallengeChallengeAttempt和User三个领域类。设计时可以搭配Challenge和ChallengeAttempt之间的联系为了保持域的简单性可以将Challenge的两个参数factorA和factorB都复制到ChallengeAttempt对象中这就使对象之间只建立了一种模型关系ChallengeAttempt属于特定用户。前面就是这么做的。 在模型简化过程中还可以更进一步ChallengeAttempt对象中也包括User的数据这样唯一需要存储的对象就是ChallengeAttempt。这样可以在同一张表中使用用户别名来查询数据。这样随着时间的推移Users域会不断演化而且会和其他域进行交互在数据层产生紧耦合混用不同域并不是一个好方法。 还有一种设计选择。可以将域分成两个部分Users和Challenges来映射3个单独的对象将Challenge和ChallengeAttempt及其关系划分为Challenges域。如图所示 #mermaid-svg-4AbRtVT6q7Xux0Gh {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-4AbRtVT6q7Xux0Gh .error-icon{fill:#552222;}#mermaid-svg-4AbRtVT6q7Xux0Gh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4AbRtVT6q7Xux0Gh .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-4AbRtVT6q7Xux0Gh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4AbRtVT6q7Xux0Gh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4AbRtVT6q7Xux0Gh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4AbRtVT6q7Xux0Gh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4AbRtVT6q7Xux0Gh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4AbRtVT6q7Xux0Gh .marker.cross{stroke:#333333;}#mermaid-svg-4AbRtVT6q7Xux0Gh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4AbRtVT6q7Xux0Gh .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-4AbRtVT6q7Xux0Gh .attributeBoxOdd{fill:#ffffff;stroke:#9370DB;}#mermaid-svg-4AbRtVT6q7Xux0Gh .attributeBoxEven{fill:#f2f2f2;stroke:#9370DB;}#mermaid-svg-4AbRtVT6q7Xux0Gh .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-4AbRtVT6q7Xux0Gh .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-4AbRtVT6q7Xux0Gh .relationshipLine{stroke:#333333;}#mermaid-svg-4AbRtVT6q7Xux0Gh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ChallengerAttempt User Challenger attempt corresponding 可以使用与User相同的方式完成设计。这样处理类似于DTO模式。 对于是否将DTO、领域类和数据类完全隔离存在不同意见。实践应用中更高的隔离度可替换数据层的实现而不必修改服务层的代码这可能是期望获得的但是这可能引入大量重复代码变得更复杂。 现在可以将相同的类重用于域和数据表示形式仍然可以保持域隔离下面的模型表示了数据库中的对象和关系 #mermaid-svg-5ekSEgk1A9GnWJBj {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-5ekSEgk1A9GnWJBj .error-icon{fill:#552222;}#mermaid-svg-5ekSEgk1A9GnWJBj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5ekSEgk1A9GnWJBj .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-5ekSEgk1A9GnWJBj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5ekSEgk1A9GnWJBj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5ekSEgk1A9GnWJBj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5ekSEgk1A9GnWJBj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5ekSEgk1A9GnWJBj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5ekSEgk1A9GnWJBj .marker.cross{stroke:#333333;}#mermaid-svg-5ekSEgk1A9GnWJBj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5ekSEgk1A9GnWJBj .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-5ekSEgk1A9GnWJBj .attributeBoxOdd{fill:#ffffff;stroke:#9370DB;}#mermaid-svg-5ekSEgk1A9GnWJBj .attributeBoxEven{fill:#f2f2f2;stroke:#9370DB;}#mermaid-svg-5ekSEgk1A9GnWJBj .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-5ekSEgk1A9GnWJBj .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-5ekSEgk1A9GnWJBj .relationshipLine{stroke:#333333;}#mermaid-svg-5ekSEgk1A9GnWJBj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ChallengerAttempt User attempt 选择数据库 SQL与NoSQL 市场上有很多数据库引擎选择归结为 SQL 和 NoSQL 数据库。SQL和NoSQL与不同类型的数据库交互。SQL是用于与关系数据库Relational Databases交互的方法而NoSQL是用于与非关系型数据库Relational Databases交互的方法。 SQL 数据库一直是一种行之有效的选择。它们由高度结构化的表格组成由行和列组成通过共同的属性相互关联。每列都需要为其对应的行设置一个值。键定义了表之间的关系。键是表字段列其包含每条记录的唯一值。如果将一个字段定义为表的主键则该字段可以包含在多个表中并且可以用于同时访问不同的表。一旦使用主键将其表连接到另一个表它将在另一个表中被称为外键。 虽然 SQL 数据库的标准化模式使它们变得僵硬且难以修改但它确实具有一些优势。添加到数据库的所有数据都必须符合众所周知的由行和列组成的链接表模式。有些人可能会发现这种局限性但当数据一致性、完整性、安全性和合规性非常重要时它会很有帮助。 SQL 数据库符合ACID特性即原子性、一致性、隔离性和持久性 。 原子性对数据和事务的所有更改都完全执行并作为单个操作。如果这不可能则不会执行任何更改即要么全有要么全无。一致性数据在事务开始和结束时必须有效且一致。隔离性事务同时运行不相互竞争。相反它们表现得好像是连续发生的。持久性当一个事务完成时其关联的数据是永久的不能更改。 与关系数据库不同非关系型数据库——NoSQL数据库——并不以表和记录的形式存储数据。NoSQL真正的意义是它打破了关系表的束缚能够同时存储和访问所有结构化和非结构化数据类型。而且NoSQL非常灵活且易于开发人员使用和修改。相反在这些类型的数据库中针对特定的要求设计和优化数据存储结构。 NoSQL数据库不使用关系数据库所使用的SQL而是使用对象关系映射ORM来促进与其数据的通信NoSQL非常灵活且易于开发人员使用和修改。 NoSQL数据库的四种流行类型为列存储数据库、文档型数据库、键值数据库和图形数据库。这些类型可以单独使用或组合使用。选择将取决于你的应用和你需要存储的数据类型。 那么该如何在SQL和NoSQL数据库之间进行选择呢 关于这个问题需要考虑四个方面灵活性、可扩展性、一致性和现有技术。 灵活性有时需要——当数据具有不同的结构和不同的类型时。根据定义NoSQL数据库提供了更多的自由来设计模式并在同一个数据库中存储不同的数据结构。然而SQL数据库的结构和模式则比较严格。可扩展性见过日本停车场电梯吗它允许车辆彼此叠置停放。现在有一个问题在当前的电梯上加层以及建造新的电梯哪个更有效SQL数据库是可以垂直扩展的这意味着可以给它添加级层增加其负载而NoSQL数据库是可以水平扩展的可以通过将工作分给多台服务器来增加其负载。一致性SQL数据库具有高度一致的设计。然而基于DBMSNoSQL数据库可以是一致的也可以是不一致的。例如MongoDB是一致的而Cassandra之类的数据库则不一致。现有技术可能会考虑的一个方面是数据库技术的当前发展阶段。由于SQL数据库已经存在了很长时间所以它比NoSQL数据库更发达。因此对于初学者来说从SQL开始然后转向NoSQL可能是最佳选择。 根据经验如果正在处理RDBMS关系数据库管理系统想分析数据的行为或构建自定义的仪表盘则SQL是更好的选择。此外SQL通常可以更快地进行数据存储和恢复并且更好地处理复杂的查询。 另一方面如果想在RDBMS的标准结构上进行扩展或者需要创建灵活的模式那么NoSQL数据库是更好的选择。当要存储和日志记录的数据来自分布式数据源或者只是需要临时存储的时候NoSQL数据库也是更好的选择。 SQL数据库比较古老因此研究较多固定模式设计和结构也比较成熟。NoSQL数据库由于模式灵活因此易于扩展、灵活使用起来也相对简单。 这里的数据模型是关系型的而且不打算处理数百万个并发的读写操作为Web应用程序选择一个SQL数据库以便从ACID特性中受益。 无论如何使应用程序微服务保持足够小可以在需要时更改数据库引擎而不会对整个软件体系结构产生重大影响。 H2、Hibernate和JPA 那么应该选择哪种数据库呢MySQL、SQL Server、PostgreSQL、H2、Oracle等。这里选择H2数据库引擎因为它比较小易于安装很容易嵌入应用程序中而且Spring Boot就集成了H2。 Hibernate是一个开放源代码的对象关系映射ORM框架它对JDBC进行了非常轻量级的对象封装它将 POJO与数据库表建立映射关系是一个全自动的 orm 框架hibernate 可以自动生成 SQL 语句自动执行使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。 JPA的全称是Java Persistence API 即Java 持久化API是SUN公司推出的一套基于ORM的规范内部是由一系列的接口和抽象类构成。JPA通过JDK 5.0注解描述对象关系表的映射关系并将运行期的实体对象持久化到数据库中。JPA规范本质上就是一种ORM规范注意不是ORM框架因为JPA并未提供ORM实现它只是制订了一些规范提供了一些编程的API接口但具体实现则由服务厂商来提供实现。 JPA 和 Hibernate 的关系就像 JDBC 和 JDBC 驱动的关系JPA是规范Hibernate除了作为ORM框架之外它也是一种JPA实现。 规范和实现之间的这种松耦合提供了一个明显的优势可以无缝更换不同的数据库引擎因为Spring Boot已经提供了相关配置。 Spring Boot Data JPA 依赖关系和自动配置 Spring有许多模块可处理数据库属于Spring Data系列JDBC、Cassandra、Hadoop、Elasticsearch等。其中一个就是Spring Data JPA。 Spring Data JPA是Spring基于Spring Data框架对于JPA规范的一套具体实现方案使用Spring Data JPA可以极大地简化JPA 的写法几乎可以在不写具体实现的情况下完成对数据库的操作并且除了基础的CRUD操作外Spring Data JPA还提供了诸如分页和排序等常用功能的实现方案。合理的使用Spring Data JPA可以极大的提高我们的日常开发效率和有效的降低项目开发成本。 要使用Spring Data JPA只需要添加spring-boot-starter-data-jpa依赖即可它还可以自动配置嵌入式数据库H2。 修改pom.xml文件添加依赖项spring-boot-starter-data-jpa和H2其中H2只需要运行时支持即可代码如下 dependencies...dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-jpa/artifactId/dependencydependencygroupIdcom.h2database/groupIdartifactIdh2/artifactIdscoperuntime/scope/dependency..../dependenciesHibernate是Spring Data JPA的参考实现启动程序会将Hibernate依赖引入还包括核心JPA构件以及父模块Spring Data JPA的依赖项。依赖如图所示 H2可充当嵌入式数据库不需要自己安装、启动或关闭数据库Spring Boot应用程序将控制其生命周期。如果想从外部访问数据库可在application.properties文件中添加如下配置来启用H2数据库控制台 spring.h2.console.enabledtrue重新启动应用可以看到控制台日志 2023-11-24T09:46:37.95808:00 INFO 44412 --- [ main] c.z.m.MultiplicationApplication : Starting MultiplicationApplication using Java 21 with PID 44412 (Z:\_Learn\multiplication\target\classes started by Juli ZHANG in Z:\_Learn\multiplication) 2023-11-24T09:46:37.96108:00 INFO 44412 --- [ main] c.z.m.MultiplicationApplication : No active profile set, falling back to 1 default profile: default 2023-11-24T09:46:38.37508:00 INFO 44412 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. 2023-11-24T09:46:38.39008:00 INFO 44412 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 9 ms. Found 0 JPA repository interfaces. 2023-11-24T09:46:38.72908:00 INFO 44412 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2023-11-24T09:46:38.73508:00 INFO 44412 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2023-11-24T09:46:38.73508:00 INFO 44412 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.15] 2023-11-24T09:46:38.80908:00 INFO 44412 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2023-11-24T09:46:38.80908:00 INFO 44412 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 821 ms 2023-11-24T09:46:38.82708:00 INFO 44412 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2023-11-24T09:46:38.95708:00 INFO 44412 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: urljdbc:h2:mem:1551ad8d-392a-4d32-8b4c-3038a09c42cc userSA 2023-11-24T09:46:38.95808:00 INFO 44412 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2023-11-24T09:46:38.96708:00 INFO 44412 --- [ main] o.s.b.a.h2.H2ConsoleAutoConfiguration : H2 console available at /h2-console. Database available at jdbc:h2:mem:1551ad8d-392a-4d32-8b4c-3038a09c42cc 2023-11-24T09:46:39.04708:00 INFO 44412 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2023-11-24T09:46:39.08008:00 INFO 44412 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.2.13.Final 2023-11-24T09:46:39.08108:00 INFO 44412 --- [ main] org.hibernate.cfg.Environment : HHH000406: Using bytecode reflection optimizer 2023-11-24T09:46:39.26008:00 INFO 44412 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer 2023-11-24T09:46:39.47608:00 INFO 44412 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set hibernate.transaction.jta.platform to enable JTA platform integration) 2023-11-24T09:46:39.47908:00 INFO 44412 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit default 2023-11-24T09:46:39.50908:00 WARN 44412 --- [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning 2023-11-24T09:46:39.72208:00 INFO 44412 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path 2023-11-24T09:46:39.72708:00 INFO 44412 --- [ main] c.z.m.MultiplicationApplication : Started MultiplicationApplication in 1.987 seconds (process running for 2.39)从日志中可以看到Spring Boot会配置H2数据库并提供H2控制台访问页面现在可以在浏览器地址栏中输入http://localhost:8080/h2-console来查看H2控制台 在JDBC URL栏输入前面日志中的数据库单击Connect按钮就可以连接数据库了。如图所示 Spring Data JPA技术栈 如图所示 #mermaid-svg-C0WmUCan8qVmpn2S {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-C0WmUCan8qVmpn2S .error-icon{fill:#552222;}#mermaid-svg-C0WmUCan8qVmpn2S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C0WmUCan8qVmpn2S .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-C0WmUCan8qVmpn2S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C0WmUCan8qVmpn2S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C0WmUCan8qVmpn2S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C0WmUCan8qVmpn2S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C0WmUCan8qVmpn2S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C0WmUCan8qVmpn2S .marker.cross{stroke:#333333;}#mermaid-svg-C0WmUCan8qVmpn2S svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C0WmUCan8qVmpn2S .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-C0WmUCan8qVmpn2S .cluster-label text{fill:#333;}#mermaid-svg-C0WmUCan8qVmpn2S .cluster-label span{color:#333;}#mermaid-svg-C0WmUCan8qVmpn2S .label text,#mermaid-svg-C0WmUCan8qVmpn2S span{fill:#333;color:#333;}#mermaid-svg-C0WmUCan8qVmpn2S .node rect,#mermaid-svg-C0WmUCan8qVmpn2S .node circle,#mermaid-svg-C0WmUCan8qVmpn2S .node ellipse,#mermaid-svg-C0WmUCan8qVmpn2S .node polygon,#mermaid-svg-C0WmUCan8qVmpn2S .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-C0WmUCan8qVmpn2S .node .label{text-align:center;}#mermaid-svg-C0WmUCan8qVmpn2S .node.clickable{cursor:pointer;}#mermaid-svg-C0WmUCan8qVmpn2S .arrowheadPath{fill:#333333;}#mermaid-svg-C0WmUCan8qVmpn2S .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-C0WmUCan8qVmpn2S .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-C0WmUCan8qVmpn2S .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-C0WmUCan8qVmpn2S .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-C0WmUCan8qVmpn2S .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-C0WmUCan8qVmpn2S .cluster text{fill:#333;}#mermaid-svg-C0WmUCan8qVmpn2S .cluster span{color:#333;}#mermaid-svg-C0WmUCan8qVmpn2S div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-C0WmUCan8qVmpn2S :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 实现 使用 实现 使用 SimpleJpaRepository JpaRepository EntityManage SessionImpl Java API-Hikari实现 H2 最底层Java API-Hakari包含了java.sql和javax.sql中一些核心Java API用于处理SQL数据库。这里可以找到DataSource、Connection以及其他用于池化资源的接口如PooledConnection或ConnectionPoolDataSource可以找到不同供应商对这些API的多种实现。Spring Boot带有的HiKariCP是DATa Source连接池最流行的一种实现轻量且性能良好。Hikari 在日语中的含义是光作者特意用这个含义来表示这块数据库连接池真的速度很快。Hikari 最引以为傲的就是它的性能。 Hibernate使用这些API以及应用程序中的HikariCP来连接H2数据库。Hibernate中用于管理数据库的JPA风格是SessionImpl类包含大量代码来执行语句、执行查询、处理会话的连接等。这个类通过继承来实现JPA接口EntityManager这是JPA规范的一部分Hibernate中有其完整实现。 Spring Data JPA在JPA的EntityManager上定义了JpaRepository接口包含最常用的方法find、get、delete、update等。SimpleJpaReposity是其默认实现并使用EntityManager这意味着不需要使用纯JPA标准或Hibernate即可使用Spring。 数据源自动配置 当使用新的依赖重新执行应用程序时会发现并没有配置数据源却能成功打开H2数据库并连接这就是自动配置的功能。 通常可以使用application.properties来配置数据源这些属性由Spring Boot中的DataSourceProperties类定义其中包含数据库的URL、用户名和密码等。下面是DataSourceProperties类的关于用户名sa的代码片段 /*** Determine the username to use based on this configuration and the environment.* return the username to use* since 1.4.0*/public String determineUsername() {if (StringUtils.hasText(this.username)) {return this.username;}if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())) {return sa;}return null;}Spring Boot开发者知道这些惯例进行了预设因此数据库开箱即用。这里使用的H2是内存中的数据库关闭应用程序时所有测试数据都会丢失。当然也可以通过设置DB_CLOSE_ON_EXITFALSE禁用自动关闭让Spring Boot决定何时关闭数据库。 通常情况下需要开发者配置数据库例如 # 访问H2数据库的web控制台 spring.h2.console.enabledtrue # 使用指定文件作为数据源 spring.datasource.urljdbc:h2:file:~/multiplication;DB_CLOSE_ON_EXITFALSE # 在创建或修改实体时创建和更新数据库表 spring.jpa.hibernate.ddl-autoupdate # 在控制台日志中显示数据库操作的SQL语句 spring.jpa.show-sqltrue实体 从数据角度看JPA将实体称为Java对象根据前面的分析将存储User和ChallengeAttempt就必须将它们定义为实体。 下面为User类添加一些注解代码如下 package cn.zhangjuli.multiplication.user;import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import lombok.*;/*** author Juli Zhang, a hrefmailto:zhjllut.edu.cnContact me/a br*/ // 将该类标记为要映射到数据库的对象如果希望使用不同的名称可以在注解中嵌入值。 // 可以使用JPA的Transient注解来排除字段。 Entity // 聚合了equals方法、hashCode方法、toString、getter和setter非常适合实体类。 Data AllArgsConstructor // JPA和Hibernate要求实体具有默认的空构造方法。 NoArgsConstructor // 数据库中可能存在user这里改变一下表名防止出现标识符错误。 Table(name s_user) public class User {// 唯一标识Id// 为一个实体生成一个唯一标识的主键(JPA要求每一个实体Entity,必须有且只有一个主键),// GeneratedValue提供了主键的生成策略。GeneratedValueprivate Long id;private String alias;public User(final String userAlias) {this(null, userAlias);} }对于ChallengeAttempt类修改如下 package cn.zhangjuli.multiplication.challenge;import cn.zhangjuli.multiplication.user.User; import jakarta.persistence.*; import lombok.*;/*** author Juli Zhang, a hrefmailto:zhjllut.edu.cnContact me/a br*/ // 将该类标记为要映射到数据库的对象如果希望使用不同的名称可以在注解中嵌入值。 // 可以使用JPA的Transient注解来排除字段。 Entity // 聚合了equals方法、hashCode方法、toString、getter和setter非常适合实体类。 Data AllArgsConstructor // JPA和Hibernate要求实体具有默认的空构造方法。 NoArgsConstructor public class ChallengeAttempt {// 唯一标识Id// 为一个实体生成一个唯一标识的主键(JPA要求每一个实体Entity,必须有且只有一个主键),// GeneratedValue提供了主键的生成策略。GeneratedValueprivate Long id;// 多对一关系。FetchType告诉Hibernate何时为嵌入的User字段收集存储在不同表中的值。// 如果为EAGERUser数据会在收集ChallengeAttempt数据时一起收集。// 如果为LAZY只有当ChallengeAttempt访问这个字段时才会执行检索这个字段的查询。// 这里在收集ChallengeAttempt时不需要用户的数据。ManyToOne(fetch FetchType.LAZY)// 用1个列来连接2个表。这会转换为CHALLENGE_ATTEMPT表的新列USER_ID对应USER表的id。JoinColumn(name USER_ID)private User user;private int factorA;private int factorB;private int resultAttempt;private boolean correct; }使用惰性关联可以避免触发对无关数据的额外查询。 将领域类重用为实体应该注意 JPA和Hibernate需要在类中添加setter和一个空的构造方法。这很不方便会妨碍创建遵循“不变性”等良好实践的类。或者说领域类被数据需求破坏了。 当构建小型应用程序且知道这些原因时问题不大只需要避免在代码中使用setter或空构造方法即可。但对于大型团队合作或项目这就是一个问题了。这种情况下可以考虑拆分域和实体类。这会带来一些代码重复但可以实施良好实践。 存储库 遵循领域启动设计使用存储库来连接数据库JPA存储库和Spring Data JPA包含了相应的功能。 Spring的SimpleJpaRepository类使用JPA的EntityManager来管理数据库对象而且还增加了一些特性如分页和排序等比普通JPA接口更方便。 下面就来实现ChallengeAttemptRepository接口代码如下 package cn.zhangjuli.multiplication.challenge;import org.springframework.data.repository.CrudRepository;import java.util.List;/*** 继承了Spring Data Common中的CrudRepository接口CrudRepository定义了创建、读取、更新和删除对象的基本方法。* author Juli Zhang, a hrefmailto:zhjllut.edu.cnContact me/a br*/ public interface ChallengeAttemptRepository extends CrudRepositoryChallengeAttempt, Long {/*** 根据用来别名查找top10根据id逆序排列* param userAlias 用户别名* return the last 10 attempts for a given user, identified by their alias.*/ListChallengeAttempt findTop10ByUserAliasOrderByIdDesc(String userAlias); }ChallengeAttemptRepository 接口继承了Spring Data Common中的CrudRepository接口CrudRepository定义了创建、读取、更新和删除对象的基本方法。Spring Data JPA中的SimpleJpaRepository类也实现了此接口。除了CrudRepository还有其他两种选择 如果选择扩展普通的Repository接口就没有CRUD功能。但是如果不想使用默认方法而是想微调CrudRepository中公开的方法时可以用它来注解。如果还需要分页和排序可扩展PagingAndSortingRepository这能提供更好的块处理或分页查询。 Spring Data中可以通过在方法名称中使用命名约定来创建定义查询的方法Spring Data会处理接口中定义的方法检索其中没有明确定义查询且符合命名约定的方法来创建查询方法然后解析方法名称将其分解为块并构建一个与该定义相对应的JPA查询。 有时想执行一些查询方法无法实现的查询就需要自定义查询了可使用Java持久性查询语言JPQL来编写查询如下所示 /*** 根据用来别名查找后几个ChallengeAttempt根据id逆序排列* param userAlias 用户别名* return the last attempts for a given user, identified by their alias.*/Query(SELECT a FROM ChallengeAttempt a WHERE a.user.alias ?1 ORDER BY a.id DESC)ListChallengeAttempt lastAttempts(String userAlias);这很像标准的SQL区别如下 没有用表名而是使用类名。关联字段没有使用列而是使用对象字段使用点遍历对象结构a.user.alias。可使用参数占位符如例子中的?1来引用第一个也是唯一一个传递的参数。 下面来实现User存储库即UserRepository接口如下所示 package cn.zhangjuli.multiplication.user;import org.springframework.data.repository.CrudRepository;import java.util.Optional;/*** author Juli Zhang, a hrefmailto:zhjllut.edu.cnContact me/a br*/ public interface UserRepository extends CrudRepositoryUser, Long {OptionalUser findByAlias(final String alias); }如果存在匹配项findByAlias将返回一个封装在Optional中的User如果没有则返回一个空的Optional对象。 这两个存储库已经包含了管理数据库实体所需的一切不需要实现这些接口甚至不需要添加Repository注解。Spring通过Data模块将找到所有扩展了基本接口的接口注入所需的Bean。 存储User和ChallengeAttempt 完成数据层后就可以在服务层使用这些存储库了。 首先用新的预期逻辑扩展测试用例 无论ChallengeAttempt是否正确都会存储。如果是给定用户的第一个ChallengeAttempt有别名Alias标识应该创建该用户如果别名存在则ChallengeAttempt应该关联到已经存在的用户。 这样需要对ChallengeServiceTest进行更新 package cn.zhangjuli.multiplication.challenge;import cn.zhangjuli.multiplication.user.UserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock;import static org.assertj.core.api.BDDAssertions.then; import static org.mockito.BDDMockito.given; import static org.mockito.AdditionalAnswers.returnsFirstArg; import static org.mockito.ArgumentMatchers.any;/*** author Juli Zhang, a hrefmailto:zhjllut.edu.cnContact me/a br*/ ExtendWith(MockitoExtension.class) public class ChallengeServiceTest {private ChallengeService challengeService;// 使用Mockito进行模拟Mockprivate UserRepository userRepository;Mockprivate ChallengeAttemptRepository challengeAttemptRepository;BeforeEachpublic void setUp() {challengeService new ChallengeServiceImpl(userRepository,challengeAttemptRepository);given(challengeAttemptRepository.save(any())).will(returnsFirstArg());}//... }下面代码进行正确尝试的测试 Testpublic void checkCorrectAttemptTest() {// given// 这里希望save方法什么都不做只返回第一个也是唯一一个传递的参数这样不必调用真实的存储库即可测试该层。given(attemptRepository.save(any())).will(returnsFirstArg());ChallengeAttemptDTO attemptDTO new ChallengeAttemptDTO(50, 60, john_doe, 3000);// whenChallengeAttempt resultAttempt challengeService.verifyAttempt(attemptDTO);// thenthen(resultAttempt.isCorrect()).isTrue();verify(userRepository).save(new User(john_doe));verify(attemptRepository).save(resultAttempt);}这里添加了一个新用例用来验证来自同一用户的更多ChallengeAttempt并不会创建新的用户实体而是重用现有实体。代码如下 Testpublic void checkExistingUserTest() {// givengiven(attemptRepository.save(any())).will(returnsFirstArg());User existingUser new User(1L, john_doe);given(userRepository.findByAlias(john_doe)).willReturn(Optional.of(existingUser));ChallengeAttemptDTO attemptDTO new ChallengeAttemptDTO(50, 60, john_doe, 5000);// whenChallengeAttempt resultAttempt challengeService.verifyAttempt(attemptDTO);// thenthen(resultAttempt.isCorrect()).isFalse();then(resultAttempt.getUser()).isEqualTo(existingUser);verify(userRepository, never()).save(any());verify(attemptRepository).save(resultAttempt);}现在无法编译该测试类ChallengeService中需要提供两个repository修改ChallengeServiceImpl类代码如下 package cn.zhangjuli.multiplication.challenge;import cn.zhangjuli.multiplication.user.User; import cn.zhangjuli.multiplication.user.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service;/*** author Juli Zhang, a hrefmailto:zhjllut.edu.cnContact me/a br*/ Service RequiredArgsConstructor Slf4j public class ChallengeServiceImpl implements ChallengeService {private final UserRepository userRepository;private final ChallengeAttemptRepository attemptRepository;Overridepublic ChallengeAttempt verifyAttempt(ChallengeAttemptDTO attemptDTO) {// Check if the attempt is correctboolean isCorrect attemptDTO.getGuess() attemptDTO.getFactorA() * attemptDTO.getFactorB();// 检查alias用户是否存在不存在就创建User user userRepository.findByAlias(attemptDTO.getUserAlias()).orElseGet(() - {log.info(Creating new user with alias {}, attemptDTO.getUserAlias());return userRepository.save(new User(attemptDTO.getUserAlias()));});// Builds the domain object. Null id for now.ChallengeAttempt checkedAttempt new ChallengeAttempt(null,user,attemptDTO.getFactorA(),attemptDTO.getFactorB(),attemptDTO.getGuess(),isCorrect);// Stores the attemptreturn attemptRepository.save(checkedAttempt);} }现在测试就可以通过了。 repository测试 没有为应用程序的数据层创建测试因为这没有多大意义这里并没有编写任何实现。 显示最近的ChallengeAttempt 已经修改了ChallengeServiceImpl的服务逻辑来存储User和ChallengeAttempt还缺少一些功能获取最近的ChallengeAttempt并显示在页面上。 服务层可以简单地使用存储库中的查询方法在控制器层创建一个新的REST API以通过别名来获取ChallengeAttempt。 服务层 在ChallengeService接口中添加getStatisticsForUser方法代码如下 package cn.zhangjuli.multiplication.challenge;import java.util.List;/*** author Juli Zhang, a hrefmailto:zhjllut.edu.cnContact me/a br*/ public interface ChallengeService {/*** verifies if an attempt coming from the presentation layer is correct or not.** param resultAttempt a DTO(Data Transfer Object) object* return the resulting ChallengeAttempt object*/ChallengeAttempt verifyAttempt(ChallengeAttemptDTO resultAttempt);/*** Gets the statistics for a given user.** param userAlias the users alias* return a list of the last 10 {link ChallengeAttempt}* objects created by the user.*/ListChallengeAttempt getStatisticsForUser(final String userAlias); }在ChallengeServiceTest中编写测试代码 Testpublic void retrieveStatisticsTest() {// givenUser user new User(john_doe);ChallengeAttempt attempt1 new ChallengeAttempt(1L, user, 50, 60, 3010, false);ChallengeAttempt attempt2 new ChallengeAttempt(2L, user, 50, 60, 3051, false);ListChallengeAttempt lastAttempts List.of(attempt1, attempt2);given(attemptRepository.findTop10ByUserAliasOrderByIdDesc(john_doe)).willReturn(lastAttempts);// whenListChallengeAttempt latestAttemptsResult challengeService.getStatisticsForUser(john_doe);// thenthen(latestAttemptsResult).isEqualTo(lastAttempts);}实现这个方法很简单ChallengeServiceImpl中调用repository即可 package cn.zhangjuli.multiplication.challenge;import cn.zhangjuli.multiplication.user.User; import cn.zhangjuli.multiplication.user.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service;import java.util.List;/*** author Juli Zhang, a hrefmailto:zhjllut.edu.cnContact me/a br*/ Service RequiredArgsConstructor Slf4j public class ChallengeServiceImpl implements ChallengeService {// ...Overridepublic ListChallengeAttempt getStatisticsForUser(final String userAlias) {return attemptRepository.findTop10ByUserAliasOrderByIdDesc(userAlias);} }运行测试可以通过。 控制器层 现在需要从控制器层连接服务层。这需要通过用户别名alias来查询ChallengeAttempt要使用查询参数alias实现很简单调用ChallengeService的方法即可代码如下 package cn.zhangjuli.multiplication.challenge;import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*;import java.util.List;/*** author Juli Zhang, a hrefmailto:zhjllut.edu.cnContact me/a br*/ Slf4j RequiredArgsConstructor RestController RequestMapping(/attempts) public class ChallengeAttemptController {private final ChallengeService challengeService;PostMappingResponseEntityChallengeAttempt postResult(RequestBody Valid ChallengeAttemptDTO challengeAttemptDTO) {return ResponseEntity.ok(challengeService.verifyAttempt(challengeAttemptDTO));}GetMappingResponseEntityListChallengeAttempt getStatistics(RequestParam String alias) {return ResponseEntity.ok(challengeService.getStatisticsForUser(alias));} }类似前面的测试可以编写测试用例代码如下 Testpublic void getUserStatistics() throws Exception {// givenUser user new User(john_doe);ChallengeAttempt attempt1 new ChallengeAttempt(1L, user, 50, 70, 3500, true);ChallengeAttempt attempt2 new ChallengeAttempt(2L, user, 20, 10, 210, false);ListChallengeAttempt recentAttempts List.of(attempt1, attempt2);given(challengeService.getStatisticsForUser(john_doe)).willReturn(recentAttempts);// whenMockHttpServletResponse response mockMvc.perform(get(/attempts).param(alias, john_doe)).andReturn().getResponse();// thenthen(response.getStatus()).isEqualTo(HttpStatus.OK.value());then(response.getContentAsString()).isEqualTo(jsonResultAttemptList.write(recentAttempts).getJson());}重新启动应用程序输入如下命令 http POST :8080/attempts factorA50 factorB60 userAliasnoise guess5302 HTTP/1.1 200 Connection: keep-alive Content-Type: application/json Date: Fri, 24 Nov 2023 09:43:48 GMT Keep-Alive: timeout60 Transfer-Encoding: chunked Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers{correct: false,factorA: 50,factorB: 60,id: 1,resultAttempt: 5302,user: {alias: noise,id: 1} }http :8080/attempts?aliasnoise HTTP/1.1 500 Connection: close Content-Type: application/json Date: Fri, 24 Nov 2023 09:49:58 GMT Transfer-Encoding: chunked Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers{error: Internal Server Error,path: /attempts,status: 500,timestamp: 2023-11-24T09:49:58.44700:00 }可以看到REST API接口的响应查询数据库也可以发现已经存储了执行的结果。但是执行查询时会产生一个服务器错误。在后端日志中可以找到对应的异常。这是ByteBuddyInterceptor造成的主要是将User配置为LAZY了如果是EAGER就不会发生这样的错误这不是要的解决方案。 要继续使用LAZY模式第一种方法是自定义JSON序列化使其能处理Hibernate对象。这需要在pom.xml中添加jackson-datatype-hibernate依赖 dependencygroupIdcom.fasterxml.jackson.datatype/groupIdartifactIdjackson-datatype-hibernate5/artifactId/dependency!-- --dependencygroupIdjavax.persistence/groupIdartifactIdpersistence-api/artifactIdversion1.0.2/version/dependency接着需要为Jackson的新Hibernate模块创建一个BeanSpring Boot的Jackson2ObjectMapperBuilder会通过自动配置来使用它下面是配置代码 package cn.zhangjuli.multiplication.configuration;import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;/*** author Juli Zhang, a hrefmailto:zhjllut.edu.cnContact me/a br*/ Configuration public class JsonConfiguration {Beanpublic Module hibernateModule() {return new Hibernate5Module();} }现在启动应用程序并验证可以成功检索ChallengeAttempt如下所示 http :8080/attempts?aliasnoise HTTP/1.1 200 Connection: keep-alive Content-Type: application/json Date: Sat, 25 Nov 2023 01:04:17 GMT Keep-Alive: timeout60 Transfer-Encoding: chunked Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers[{correct: false,factorA: 50,factorB: 60,id: 1,resultAttempt: 5302,user: null} ]另一种替代方法是在application.properties中添加Jackson序列化特性也可以解决该问题。重新运行应用程序可得到如下结果 spring.jackson.serialization.fail_on_empty_beansfalsehttp GET :8080/attempts?aliasnoise HTTP/1.1 200 Connection: keep-alive Content-Type: application/json Date: Fri, 24 Nov 2023 09:56:29 GMT Keep-Alive: timeout60 Transfer-Encoding: chunked Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers[{correct: false,factorA: 50,factorB: 60,id: 1,resultAttempt: 5302,user: {alias: noise,id: 1}} ]这里出现了非预期的输出从控制台日志可以发现序列器获取了用户数据并触发了Hibernate的额外查询来获取数据这样LAZY参数就失效了。日志如下 Hibernate: select c1_0.id,c1_0.correct,c1_0.factora,c1_0.factorb,c1_0.result_attempt,c1_0.user_id from challenge_attempt c1_0 left join s_user u1_0 on u1_0.idc1_0.user_id where u1_0.alias? order by c1_0.id desc fetch first ? rows only Hibernate: select u1_0.id,u1_0.alias from s_user u1_0 where u1_0.id?因此采用第一种方式使用JSON序列化来处理延迟获取。 Spring Boot背后存在许多隐藏行为在没有真正理解其含义的情况下应该避免寻求快速的解决方案。 用户界面 最后需要将新功能集成到React前端以显示最近的ChallengeAttempt。 现在在基本界面上添加一个列表用于显示用户最近的几个ChallengeAttempt。 首先直接呈现ChallengeComponent import React from react; import ./App.css; import ChallengeComponent from ./components/ChallengeComponent;function App() {return (ChallengeComponent/); }export default App;删除原有的样式自己定义样式index.css样式如下 body {font-family: Segoe UI, Roboto, Arial, sans-serif; }App.css修改如下 .display-column {display: flex;flex-direction: column;align-items: center; }.challenge {font-size: 4em; }th {padding-right: 0.5em;border-bottom: solid 1px; }定义了基本显示需要在ApiClient.js中检索ChallengeAttempt代码如下 class ApiClient {static SERVER_URL http://localhost:8080;static GET_CHALLENGE /challenges/random;static POST_RESULT /attempts;static GET_ATTEMPTS_BY_ALIAS /attempts?alias;static challenge(): PromiseResponse {return fetch(ApiClient.SERVER_URL ApiClient.GET_CHALLENGE);}static sendGuess(user: string,a: number,b: number,guess: number): PromiseResponse {return fetch(ApiClient.SERVER_URL ApiClient.POST_RESULT, {method: POST,headers: {Content-Type: application/json},body: JSON.stringify({userAlias: user,factorA: a,factorB: b,guess: guess})});}static getAttempts(userAlias: string): PromiseResponse {return fetch(ApiClient.SERVER_URL ApiClient.GET_ATTEMPTS_BY_ALIAS userAlias);} } export default ApiClient;下面创建一个新的ReactComponent来显示ChallengeAttempt列表该组件不需要状态这里通过父组件进行最后的ChallengeAttempt。 import * as React from react;class LastAttemptsComponent extends React.Component {render() {return (tabletheadtrthChallenge/ththYour Guess/ththCorrect/th/tr/theadtbody{this.props.lastAttempts.map(a tr key{a.id} style{{color: a.correct ? green : red}}td{a.factorA} x {a.factorB}/tdtd{a.resultAttempt}/tdtd{a.correct ? Correct : (Incorrect ( a.factorA * a.factorB ))}/td/tr)}/tbody/table)} }export default LastAttemptsComponent;在渲染React组件时使用map可以轻松地遍历数组。数组的每个元素都应该使用一个key属性来帮助框架识别不断变化的元素。 同时还需要对ChallengeComponent类进行修改 import ApiClient from ../services/ApiClient; import * as React from react; import LastAttemptsComponent from ./LastAttemptsComponent;// 类从React.Component继承这就是React创建组件的方式。 // 唯一要实现的方法是render()该方法必须返回DOM元素才能在浏览器中显示。 class ChallengeComponent extends React.Component {// 构造函数初始化属性及组件的state如果需要的话// 这里创建一个state来保持检索到的挑战以及用户为解决尝试而输入的数据。constructor(props) {super(props);this.state {a: ,b: ,user: ,message: ,guess: ,lastAttempts: []};// 两个绑定方法。如果想要在事件处理程序中使用这是必要的需要实现这些方法来处理用户输入的数据。this.handleSubmitResult this.handleSubmitResult.bind(this);this.handleChange this.handleChange.bind(this);}// 这是一个生命周期方法用于首次渲染组件后立即执行逻辑。componentDidMount(): void {this.refreshChallenge();}handleChange(event) {const name event.target.name;this.setState({[name]: event.target.value});}handleSubmitResult(event) {event.preventDefault();ApiClient.sendGuess(this.state.user,this.state.a,this.state.b,this.state.guess).then(res {if (res.ok) {res.json().then(json {if (json.correct) {this.updateMessage(Congratulations! Your guess is correct);} else {this.updateMessage(Oops! Your guess json.reaultAttempt is wrong, but keep playing!);}this.updateLastAttempts(this.state.user);this.refreshChallenge();});} else {this.updateMessage(Error: server error or not available);}});}updateMessage(m: string) {this.setState({message: m});}render() {return (div classNamedisplay-columndivh3Your new challenge is/h3div classNamechallenge{this.state.a} x {this.state.b}/div/divform onSubmit{this.handleSubmitResult}labelYour alias:input typetext maxLength12 nameuservalue{this.state.user} onChange{this.handleChange}//labelbr/labelYour guess:input typenumber min0 nameguessvalue{this.state.guess} onChange{this.handleChange}//labelbr/input typesubmit valueSubmit//formh4{this.state.message}/h4{this.state.lastAttempts.length 0 LastAttemptsComponent lastAttempts{this.state.lastAttempts}/}/div);}updateLastAttempts(userAlias: string) {ApiClient.getAttempts(userAlias).then(res {if (res.ok) {let attempts: Attempt[] [];res.json().then(data {data.forEach(item {attempts.push(item);});this.setState({lastAttempts: attempts});})}})}refreshChallenge() {ApiClient.challenge().then(res {if (res.ok) {res.json().then(json {this.setState({a: json.factorA,b: json.factorB,});});} else {this.updateMessage(Cant reach the server);}});} }export default ChallengeComponent;请注意变化的地方添加了新属性lastAttempts到state中添加了2个方法updateLastAttempts和refreshChallengerender方法也进行了修改修改了样式也添加了ChallengeAttempt列表显示。 启动后端应用程序在前端应用程序控制台执行npm start命令进行体验。在浏览器地址栏输入http://localhost:3000访问页面输入尝试下面是一种体验 现在成功地完成了前端应用程序的开发。 如果关心H2数据库可以在浏览器中访问http://localhost:8080/h2-console下面是查询CHALLENGE_ATTEMPT表数据的截图 小结 文章介绍了如何持久化建模数据并使用对象关系映射ORM将领域对象转换为数据库记录讲述了使用JPA注解来映射Java类之间的关联学习使用Spring Data存储库的功能来高效编写代码的方法。通过扩展前面介绍的用户乘法测数游戏的功能扩展展示了如何实现存储库、完善服务层进而完成控制器层的REST API接口构建以及如何实现前端页面组件的构造和交互。至此已经完成了整个应用程序的构造过程。
http://www.zqtcl.cn/news/955053/

相关文章:

  • 怎么把网站链接做二维码app跟网站的区别是什么
  • 南通住房和城乡建设局网站wordpress exif
  • 在谷歌上做网站广告要多少钱萍乡网站开发
  • 资源站 wordpress仙游县住房和城乡建设局网站
  • 锦州做网站公司北京互联网公司名单
  • 免费英文 网站模板公司做网站多少钱乐器
  • 软文营销推广成都seo正规优化
  • soho建设外贸网站怎样取消网站备案
  • 建设部网站实名制举报wordpress.org去掉
  • 网站地址ip域名查询公司网站建设安全的风险
  • 盐城建设厅网站设计备案网站创建服务
  • wp如何做双语网站个人网站首页内容
  • 网络推广网站排行榜百度怎么搜索网址打开网页
  • 网站制作和如何推广深圳西乡
  • 男生女生做污事网站免费西安企业展厅设计公司
  • 做网络写手最好进那个网站网页建站需要多少钱
  • 网站打开不对摄影设计说明200字
  • 无锡网站制作公司排名网站开发与应用 大作业作业
  • 网站建设中搜索引擎wordpress 不在首页显示文章
  • 先做网站先备案嘉兴网站建设推广
  • 建设法律法规文本查询网站Html手机浏览网站变形
  • 怎么拥有个人网站wordpress做的网站
  • wordpress建什么站江苏网站建设效果
  • 建设网站网站多少钱东莞网站建设 光龙
  • 天津和平做网站哪家好搞笑网站建设目的和意义
  • 一般做网站带宽选择多大的wordpress页面侧菜单
  • 海淀青岛网站建设友情链接适用网站
  • 青海建设厅官方网站资阳seo
  • 网站个人备案 企业备案深圳高端网站建设网页设计
  • 网站广东省备案国产最好的a级suv88814