第三部分 后端中的 Spring
第十章介绍使用Spring的JDBC的抽象来查询关系型数据库。第十一章通过对象-关系映射持久化数据,与orm框架集成。十二章使用会介绍一些非关系型数据库。第十三章是介绍Spring Security在后端中的应用。
第10章 通过Spring和JDBC征服数据库
- 定义Spring对数据访问的支持
- 配置数据库资源
- 使用Spring的JDBC模板
10.1 Spring的数据访问哲学
-
Spring的数据访问也是遵循面向对象的针对接口编程。
-
DAO:data access object或repository。为了避免持久化的逻辑分散在业务的各个组件,将数据访问的功能放在一个或者多个专注于此项任务的组件中。
-
服务对象本身不处理数据访问,将这部分工作委托给Repository-> 松耦合。
-
优点
- 易于测试。当服务对象和数据处理组件解绑,测试服务对象时候可以直接提供一个mock数据库对象,不需要进行数据库连接。
- 灵活。数据访问层和持久化技术无关,切换持久化框架对于整体应用影响小。
- 使用了统一的异常处理体系(同时也存在于它支持的持久化框架中)。
10.1.1 了解Spring数据访问的异常体系
- 捕获SQLException对于问题排查和定位很重要。
- JDBC的异常体系过于简单,Hibernate的异常又是它独有的。我们希望可以获得具有描述性的、平台和持久化框架无关的异常输出。
Spring提供的与平台无关的持久化异常
对比JDBC,它的异常类型丰富许多。
JDBC的异常 | Spring的数据访问异常 |
---|---|
BatchUpdateException DataTruncation SQLException SQLWarning |
BadSqlGrammarException CannotAcquireLockException CannotSerializeTransactionException CannotGetJdbcConnectionException CleanupFailureDataAccessException ConcurrencyFailureException DataAccessException DataAccessResourceFailureException DataIntegrityViolationException DataRetrievalFailureException DataSourceLookupApiUsageException DeadlockLoserDataAccessException |
BatchUpdateException DataTruncation SQLException SQLWarning |
DuplicateKeyException EmptyResultDataAccessException IncorrectResultSizeDataAccessException IncorrectUpdateSemanticsDataAccessException InvalidDataAccessApiUsageException InvalidDataAccessResourceUsageException InvalidResultSetAccessException JdbcUpdateAffectedIncorrectNumberOfRowsException LbRetrievalFailureExceptionNonTransientDataAccessResourceException OptimisticLockingFailureException PermissionDeniedDataAccessException PessimisticLockingFailureException QueryTimeoutException RecoverableDataAccessException SQLWarningException SqlXmlFeatureNotImplementedException TransientDataAccessException TransientDataAccessResourceException TypeMismatchDataAccessException UncategorizedDataAccessException UncategorizedSQLException |
除此之外,它还与持久化框架无关。
不用写Catch!
Spring的异常都继承自DataAccessException,它是一个非检查型异常,不需要捕获。
Spring将异常处理捕获交给开发者来决定。
10.1.2 数据访问模板化
-
Spring将数据访问过程中固定的和可变的部分明确划分为两个不同的类:模板和回调。模板管理过程中固定的部分,而回调处理自定义的数据访问代码。
-
针对不同的持久化平台,Spring提供了多个可选模板。
10.2 配置数据源
10.2.1 使用JNDI数据源
-
可以像使用bean一样将jndi数据源的引用配置并装配到需要的类
xml <jee:jndi-lookup id="dataSource" jndi-name="/jdbc/SpitterDS" resource-ref="true" />
java @Bean public JndiObjectFactoryBean dataSource() { JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean(); jndiObjectFB.setJndiName("jdbc/SpittrDS"); jndiObjectFB.setResourceRef(true); jndiObjectFB.setProxyInterface(javax.sql.DataSource.class); return jndiObjectFB; }
10.2.2 使用数据源连接池
-
Spring没有提供,但是有诸如Apache Commons DBCP,c3p0,boneCP开源实现。
-
DBCP:
xml <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" p:driverClassName="org.h2.Driver" p:url="jdbc:h2:tcp://localhost/~/spitter" p:username="sa" p:password="" p:initialSize="5" p:maxActive="10" />
Java 配置
java @Bean public BasicDataSource dataSource() { BasicDataSource ds = new BasicDataSource(); ds.setDriverClassName("org.h2.Driver"); ds.setUrl("jdbc:h2:tcp://localhost/~/spitter"); ds.setUsername("sa"); ds.setPassword(""); ds.setInitialSize(5); ds.setMaxActive(10); return ds; }
什么是数据源?
JDBC2.0提供了javax.sql.DataSource接口,它负责建立与数据库的连接,当在应用程序中访问数据库时,不必编写连接数据库的代码,直接引用DataSource获取数据库的连接对象即可。用于获取操作数据的Connection对象。
什么是数据库连接池?
数据库连接池的思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。可通过设定连接池的最大连接数来防止系统无尽的与数据库连接。更重要的是我们可以通过连接池的管理机制监视数据库的连接的数量、使用情况,为系统开发、测试以及性能调整提供依据。
数据源和数据库连接池的关系?
数据源建立多个数据库连接,这些数据库连接将会保存在数据库连接池中,当需要访问数据库时,只需要从数据库连接池中获取空闲的数据库连接,当程序访问数据库结束时,数据库连接会放回数据库连接池中。
为什么要使用数据库连接池?
传统的JDBC访问数据库技术,每次访问数据库都需要通过数据库驱动器Driver和数据库名称以及密码等等资源建立数据库连接。
如此就会存在两大问题:
- 频繁的建立与断开数据库连接,会降低执行效率,增加系统资源的开销。
- 数据库的连接需要用户名和密码等资源,这些也需要一定的内存和CPU的开销。
而数据库连接池很好地解决了这些问题。
10.2.3 基于JDBC驱动的数据源
-
Spring提供了三个JDBC驱动的数据源类:
- DriverManagerDataSource:在每个连接请求时都会返回一个新建的连接。没有池化管理。
- SimpleDriveDataSource: 与前者工作方式类似。
- SingleConnectionDataSource:每个连接请求都会返回同一个连接。
它们的配置与数据源连接池的配置类似。
java @Bean public DataSource dataSource() { DriverManagerDataSource ds = new DriverManagerDataSource(); ds.setDriverClassName("org.h2.Driver"); ds.setUrl("jdbc:h2:tcp://localhost/~/spitter"); ds.setUsername("sa"); ds.setPassword(""); return ds; }
-
都没有提供池化功能。不适宜大应用,SingleConnectionDataSource不支持多线程,其它两个虽然支持但是对每个请求都会创建一个连接,开销很大。
10.2.4 使用嵌入式的数据源
- 适用于开发和测试。每次启动都会重新填充数据。
10.2.5 使用profile选择数据源
- Spring的bean profile允许我们配置在不同环境下使用不同数据源。
10.3 在Spring使用JDBC
- 使用JDBC能够较好的对数据访问的性能进行调优。
- JDBC让我们在更低层次处理数据,进行一些定制化的访问。
10.3.1 应对失控的JDBC代码
-
插入一条数据,真正在做插入操作的只有几句,其他的都是在处理数据访问。
```java private static final String SQL_INSERT_SPITTBR = "insert into spitter (username, password, fullname) values (?, ?, ?)"; private DataSource dataSource; public void addSpitter(Spitter spitter) { Connection conn = null; PreparedStatement stmt = null; try { conn = dataSource.getConnection(); stmt = conn.prepareStatement(SQL_INSERT_SPITTER); stmc.setstring(1, spicter.getUsername()); stmt.setstring(2, spitter.getPassword()); stmt.setstring(3, spitter .getFullName()); stmt.execute(); } catch (SQLException e) { // do something...not sure what, though } finally { try { if (stmt != null) { stmt.close(); }
if (conn != null) { conn.close(); } } catch (SQLException e) { // I'm even less sure about what to do here }
} } ```
-
查询数据
```java private static final String SQL_SELECT_SPITTER = "select id, username, fullname from spitter where id = ?"; public void findOne(long id) { Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = dataSource.getConnection(); stmt = conn.prepareStatement(SQL_SELECT_SPITTER); stmt.setLong(1, id); rs = stmt.executeQuery(); Spitter spitter = null; if (rs.next()) { spitter = new Spitter(); spitter.setId(rs.getLong("id")); spitter.setUsername(rs.getString("username")); spitter.setFullName(rs.getString("fullname")); } return spitter; } catch (SQLException e) { // Still not sure what I } finally { if (rs != null) { try { rs.close(); } catch (SQLException e) { } }
if (stmt != null) { try { stmt.close(); } catch (SQLException e) { } } if (conn ! = null) { try { conn.close(); } catch (SQLException e) { } }
} } ```
10.3.2 使用JDBC模板
-
Spring将数据访问的样板代码抽象到模板类之中。
- JdbcTemplate: 最基本的模板,支持简单的JDBC数据库访问以及基于索引参数的查询。
- NamedParameterJdbcTemplate:可以将值以命名参数的形式绑定到SQL。
- ~~SimpleJdbcTemplate:利用Java5的一些特性如自动装箱、泛型、可变参数列表来简化JDBC模板的使用。~~ (Spring 3.1 废弃)
-
使用JdbcTemplate来插入数据
只需为它设置DataSource
java @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }
极大简化addSpitter方法
java private void insertSpitter(Spitter spitter) { jdbcTemplate.update(INSERT_SPITTER, spitter.getUsername(), spitter.getPassword(), spitter.getFullName(), spitter.getEmail(), spitter.isUpdateByEmail()); }
-
读取
```java public Spitter findOne(long id) { return jdbcTemplate.queryForObject( SELECT_SPITTER + " where id=?", new SpitterRowMapper(), id); }
...
private static final class SpitterRowMapper implements RowMapper<Spitter> { public Spitter mapRow(ResultSet rs, int rowNum) throws SQLException { return new Spitter( rs.getLong("id"), rs.getString("username"), rs.getString("password"), rs.getString("fullname"), rs.getString("email"), rs.getBoolean("updateByEmail")); }
} ```SpitterRowMapper 对象实现了 RowMapper 接口。对于查询返回的每一行数据,JdbcTemplate 将会调用 RowMapper 的 mapRow() 方法,并传入一个 ResultSet 和包含行号的整数。在 SpitterRowMapper 的 mapRow() 方法中,我们创建了 Spitter 对象并将 ResultSet 中的值填充进去。
使用命名参数
private static final Spitter SQL_INSERT_SPITTER = "insert into spitter (username, password, fullname) " + "values (:username, :password, :fullname)";
则需要定义另外一个模板,此时sql中参数的顺序不重要:
@Bean public NamedParameterJdbcTemplate jdbcTemplate(DataSource dataSource) { return new NamedParameterJdbcTemplate(dataSource); }
addSpitter()
private static final String INSERT_SPITTER = "insert into Spitter " + " (username, password, fullname, email, updateByEmail) " + "values " + " (:username, :password, :fullname, :email, :updateByEmail)"; private void addSpitter(Spitter spitter) { Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("username", spitter.getUsername()); paramMap.put("password", spitter.getPassword()); paramMap.put("fullname", spitter.getFullName()); paramMap.put("email", spitter.getEmail()); paramMap.put("updateByEmail", spitter.isUpdateByEmail()); jdbcOperations.update(INSERT_SPITTER, paramMap); }
10.4 小结
第11章 使用对象-关系映射持久化数据
- 使用Spring和HIbernate
- 借助上下文Session,编写不依赖于Spring的Repository
- 通过Spring使用JPA
- 借助Spring Data实现自动化的JPA Repository
使用持久化数据关系映射,我们可以从查询字符串中解放,因为框架会帮我们映射对象。不但如此,我们还可以使用更复杂的特性:
- 延迟加载:只在需要的时候获取数据。假设我们在查询一组 PurchaseOrder 对象,而每个对象中都包含一个 LineItem 对象集合。如果我们只关心 PurchaseOrder 的属性,那查询出 LineItem 的数据就毫无意义。而且这可能是开销很大的操作
- 预先抓取:使用一个查询获取完整的关联对象。如果我们需要 PurchaseOrder 及其关联的 LineItem 对象,预先抓取的功能可以在一个操作中将它们全部从数据库中取出来,节省了多次查询的成本。
- 级联:更改数据库同时修改其他表(因为其之间存在诸如外键的关系)。
11.1 在Spring中集成Hibernate
11.1.1 声明Hibernate的Session工厂
-
使用Hibernate所需要的主要接口是org.hibernate.Session。session接口提供了基本的数据访问功能。
-
Hibernate Session的打开、关闭及管理有Session Factory负责。
-
Spring中提供了三个Session工厂bean给我们用以获取Session
- org.springframework.orm.hibernate3.LocalSessionFactoryBean
- org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean
- org.springframework.orm.hibernate4.LocalSessionFactoryBean
-
Session工厂的选择
-
Hibernate3.2~Hibernate4.0(不包括),并且使用XML定义映射, 你需要使用Spring 的 org.springframework.orm.hibernate3 包中的 LocalSessionFactoryBean:
java @Bean public LocalSessionFactoryBean seesionFactory(DataSource dataSource) { LocalSessionFactoryBean sfb = new LocalSessionFactoryBean(); sfb.setDataSource(dataSource); sfb.setMappingResource(new String[] {"Spitter.hbm.xml"}); Properties props = new Properties(); props.setProperty("dialect", "org.hibernate.dialect.H2Dialect"); sfb.setHibernateProperties(props); return sfb; }
-
如果倾向于使用注解方式定义持久化,且没有使用Hibernate4的话,需要用AnnotationSessionFactoryBean代替:
java @Bean public AnnotationSessionFactoryBean sessionFactory(DataSource ds) { AnnotationSessionFactoryBean sfb = new AnnotationSessionFactoryBean(); sfb.setDataSource(ds); sfb.setMappingResource(new String[] { "Spitter.hbm.xml" }); Properties props = new Properties(); props.setProperty("dialect", "org.hibernate.dialect.H2Dialect"); sfb.setHebernateProperties(props); return sfb; }
-
如果你使用HIbernate4, 那可以使用org.springframework.orm. hibernate4 中的 LocalSessionFactoryBean。他可以算是前两者的结合体。
java @Bean public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { LocalSessionFactoryBean sfb = new LocalSessionFactoryBean(); sfb.setDataSource(dataSource); sfb.setMappingResource(new String[] { "com.habuma.spittr.domain" }); Properties props = new Properties(); props.setProperty("dialect", "org.hibernate.dialect.H2Dialect"); sfb.setHebernateProperties(props); return sfb; }
-
他不再使用配置xml文件而是告诉Spring去查找一个域。里面的类可以使用JPA 的 @Entity 或 @MappedSuperclass 以及 Hibernate 的 @Entity。
-
甚至可以用全限定名的方式明确列出:
java sfb.setAnnotationClasses( new Class<?>[] {Spitter.class, Spittle.class} );
-
11.1.2 构建不依赖Spring的Hibernate代码
- 使用上下文Session会直接将Hibernate SessionFactory装配到Repository中,并使用它来获取Session。
@Repository public class HibernateSpitterRepository implements SpitterRepository { private SessionFactory sessionFactory; @Inject public HibernateSpitterRepository(SessionFatory sessionFactory) { this.sessionFactory = sessionFactory; } private Session currentSession() { return sessionFactory.getCurrentSession(); } public long count() { return findAll().size(); } public Spitter save(Spitter spitter) { Serializable id = currentSession().save(spitter); return new Spitter((Long) id, spitetr.getUsername(), spitter.getPassword(), spitter.getFullname(), spitter.getEmail(), spitter.isUpdateByEmail()); } public Spitter findOne(long id) { return (Spitter) currentSession().get(Spitter.class, id); } public Spitter findByUsername(String username) { return (Spitter) currentSession() .createCretirai(Spitter.class) .add(Retrictions.eq("username", username)) .list().get(0); } public List<Spitter> findAll() { return (List<Spitter>) currentSession() .createCriteria(Spitter.class).list; } }
-
一些代码注解
-
@Inject注解让Spring自动将一个SessionFactory注入到HibernateSpitterRepository的sessionFactory中。
-
类上使用了@Repository注解,一是为了让他被Spring组件扫描扫描到,而不用明确声明这个HibernateSpitterRepository bean,只要他在组件扫描所涵盖的包中;另外一个作用是,为不使用Hibernate 模板而使用Session的Repository添加异常转换功能。我们需要在Spring context中添加一个PersistenceExceptionTranslationPostProcessor bean:
java @Bean public BeanPostProcessor persistenceTranslation() { return new PersistenceExceptionTranslationPostProcessor(); }
他会在拥有@Repository注解的类上添加一个通知器,这样就会捕获平台相关的异常并以Spring非检查型数据访问异常重新抛出。
-
11.2 Spring与Java持久化API
11.2.1 配置实体管理工厂
- 基于JPA的应用程序需要使用EntityManagerFactory的实例来获取EntityManager实例。
- JPA定义了两种
- 应用程序管理类型:程序负责打开关闭实体管理器并在事务中对其进行控制。
- 容器管理类型:实体管理器由JavaEE创建和管理。
- 区别:配置和管理方式
- 通过应用程序控制:配置在persistence.xml中,避免了应用程序每次使用都需要定义,直接从配置文件获取。
- persistence.xml 的作用在于定义一个或多个持久化单元。持久化单元是同一个数据源下的一个或多个持久化类。
- 通过容器
- 通过应用程序控制:配置在persistence.xml中,避免了应用程序每次使用都需要定义,直接从配置文件获取。
11.2.2 编写基于JPA的Repository
-
不依赖Spring构建JPA Repository
```java package com.habuma.spittr.persistence;
import java.util.List; import javax.persistence.EntityManagerFactory; import javax.persistence.PersistenceUnit; import org.springframework.dao.DataAccessException; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import com.habuma.spittr.domain.Spitter; import com.habuma.spittr.domain.Spittle;
@Repository @Transactional public class JpsSpitterRepository implements SpitterRepository { @PersistenceUnit private EntityManagerFactory emf;
pubic void addSpitter(Spitter spitter) { emf.createEntityManager().persist(spitter); } public Spitter getSpitterById(long id) { return emf.createEntityManager().find(Spitter.class, id); } public void saveSpitter(Spitter spitter) { emf.createEntityManager().merge(spitter); } ...
} ```
- EntityManagerFactory使用了@PersistenceUnit注解,Spring会将它注入到Repository中以用来创建EntityManager。
- EntityManager每次使用都需要创建,但是有不是线程安全的,所以也不适合注入为一个单例bean。
-
利用@PersistenceContext注解
```java package com.habuma.spittr.persistence;
import java.util.List; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import org.springframework.dao.DataAccessException; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import com.habuma.spittr.domain.Spitter; import com.habuma.spittr.domain.Spittle;
@Repository @Transactional public class JpsSpitterRepository implements SpitterRepository { @PersistenceContext private EntityManager em;
public void addSpitter(Spitter spitter) { em.persist(spitter); } public Spitter getSpitterById(long id) { return em.find(Spitter.class, id); } public void saveSpitter(Spitter spitter) { em.merge(spitter); } ...
} ```
- @PersistenceContext并不会真正注入EntityManager,他没有给Repository一个EntityManager,而是给了一个EntityManager的代理。真正的EntityManager是和当前事务关联的那一个。这样就实现了线程安全。
-
@PersistenceUnit 和 @PersistenceContext 并不是 Spring 的注解,它们是由 JPA 规范提供的.
-
Transactional注解表明这个Repository中的持久化方法是在事务上下文中执行。
11.3 借助Spring Data实现自动化的JPARepository
-
像addSpitter这种方法,其中除了操作的对象不同,重复的操作和方法CRUD都需要重写很多遍在一些规模大点的程序中。
-
借助Spring Data,以接口定义的方式创建Repository
java public interface SpitterRepository extends JpsRepository<Spitter, Long> { }
-
通过配置xml,会自动实现这个Repository接口。在Spring的应用上下文创建的时候生成的。包含了继承自 JpaRepository、PagingAndSortingRepository 和 CrudRepository 的 18 个方法。
11.3.1 定义查询方法
-
通过方法签名,让Spring Data解析。
方法名 findByUsername 确定该方法需要根据 username 属性相匹配来查找 Spitter,而 username 是作为参数传递到方法中来的。另外,因为在方法签名中定义了该方法要返回一个 Spitter 对象,而不是一个集合,因此它只会查找一个 username 属性匹配的 Spitter。
Repository 方法是由一个动词、一个可选的主题(Subject)、关键词 By 以及一个断言所组成。在 findByUsername() 这个样例中,动词是 find,断言是 Username,主题并没有指定,暗含的主题是 Spitter。
-
大部分场景,主题会被省略。要查询的对象类型是通过如何参数化JpaRepository接口来确定。
在省略主题的时候,有一种例外情况。如果主题的名称以 Distinct 开头的话,那么在生成查询的时候会确保所返回结果集中不包含重复记录。
-
断言限制了结果集的属性
- IsAfter、After、IsGreaterThan、GreaterThan
- IsGreaterThanEqual、GreaterThanEqual
- IsBefore、Before、IsLessThan、LessThan
- IsLessThanEqual、LessThanEqual
- IsBetween、Between
- IsNull、Null IsNotNull、NotNull
- IsIn、In IsNotIn、NotIn
- IsStartingWith、StartingWith、StartsWith
- IsEndingWith、EndingWith、EndsWith
- IsContaining、Containing、Contains
- IsLike、Like
- IsNotLike、NotLike
- IsTrue、True
- IsFalse、False
- Is、Equals
- IsNot、Not
-
要处理 String 类型的属性时,条件中可能还会包含 IgnoringCase 或 IgnoresCase,这样在执行对比的时候就会不再考虑字符是大写还是小写
java List<Spitter> readByFirstnameOrLastname(String first, String last);
java List<Spitter> readByFirstnameIgnoringCaseOrLastnameIgnoringCase(String first, String last);
java List<Spitter> readByFirstnameOrLastnameAllIgnoresCase(String first, String last);
-
我们还可以在方法名称的结尾处添加 OrderBy实现排序结果。
java List<Spitter> readByFirstnameOrLastnameOrderByLastnameAsc(String first, String last);
11.3.2 声明自定义查询
-
有时这个语义解析集合还是不能满足需求,我们就会通过@Query自定义查询。
-
例如想找全部gmail邮箱,我们按照原来的DSL可以使用 findByEmailLike()然后传入“%gmail.com” .
-
如果能够定义一个更加便利的方法:
List<Spitter> findAllGmailSpitters();
,但是DSL不能解析会抛出异常。 -
自定义:
java @Query("select s from Spitter s where s.email like '%gmail.com'") List<Spitter> findAllGmailSpitters();
-
或者当方法名因为需要满足语义解析而特别长的时候,也最好使用一个短名字,并定义query。
11.3.3 混合自定义的功能
-
如果你需要做的事情无法通过 Spring Data JPA 来实现,那就必须要在一个比 Spring Data JPA 更低的层级上使用 JPA.
-
假设我们需要在 SpitterRepository 中添加一个方法,发表 Spittle 数量在 10,000 及以上的 Spitter 将会更新为 Elite 状态。使用 Spring Data JPA 的方法命名约定或使用 @Query 均没有办法声明这样的方法。最为可行的方案是使用如下的 eliteSweep() 方法。
```java public class SpitterRepositoryImpl implements SpitterSweeper { @PersistenceContext private ENtityManager em;
public int eliteSweep() { STring update = "UPDATE Spitter spitter " + "SET spitter.status = 'Elite' " + "WHERE spitter.status = 'Newbie' " + "AND spitter.id IN (" + "SELECT s FROM Spitter s WHERE (" + " SELECT COUNT(spittles) FROM s.spittles spittles) > 10000" + ")"; return em.createQuery(update).executeUpdate(); }
} ```
-
SpitterRepositoryImpl 并没有实现 SpitterRepository 接口。Spring Data JPA 负责实现这个接口。SpitterRepositoryImpl(将它与 Spring Data 的 Repository 关联起来的是它的名字)实现了 SpitterSweeper 接口.(当 Spring Data JPA 为 Repository 接口生成实现的时候,它还会查找名字与接口相同,并且添加了 Impl 后缀的一个类)
-
我们还需要确保 eliteSweep() 方法会被声明在 SpitterRepository 接口中。要实现这一点,避免代码重复的简单方式就是修改 SpitterRepository,让它扩展 SpitterSweeper。
11.4 小结
第12章 使用NoSQL数据库
略
第13章 缓存数据
略
第14章 保护方法应用
略