Spring Data Elasticsearch:使用Spring Data Repositories

  • 2019-08-23
  • 0
  • 0

Spring Data存储库抽象的目标是显着减少为各种持久性存储实现数据访问层所需的样板代码量。

Spring Data存储库文档和您的模块本章介绍Spring Data存储库的核心概念和接口。本章中的信息来自Spring Data Commons模块。它使用Java Persistence API(JPA)模块的配置和代码示例。您应该将XML名称空间声明和要扩展的类型调整为您使用的特定模块的等效项。“ 命名空间参考 ”涵盖XML配置,支持存储库API的所有Spring Data模块都支持XML配置。“ 存储库查询关键字 ”涵盖了存储库抽象支持的查询方法关键字。有关模块特定功能的详细信息,请参阅本文档该模块的章节。

1.1。核心概念

Spring Data存储库抽象中的中央接口是Repository。它将域类以及域类的ID类型作为类型参数进行管理。此接口主要用作标记接口,用于捕获要使用的类型,并帮助您发现扩展此接口的接口。该CrudRepository规定对于正在管理的实体类复杂的CRUD功能。示例1. CrudRepository界面

public interface CrudRepository<T, ID extends Serializable>
  extends Repository<T, ID> {

  <S extends T> S save(S entity);  
    
  Optional<T> findById(ID primaryKey); 

  Iterable<T> findAll();               

  long count();                        

  void delete(T entity);               

  boolean existsById(ID primaryKey);   

  // … more functionality omitted.
}
save(S entity)保存给定的实体。
findById(ID primaryKey)返回由给定ID标识的实体。
findAll()返回所有实体。
count()返回实体数量。
delete(T entity)删除给定的实体。
existsById(ID primaryKey)指示是否存在具有给定ID的实体。
我们还提供持久性技术特定的抽象,例如JpaRepositoryMongoRepositoryCrudRepository除了相当通用的持久性技术无关的接口之外,这些接口还扩展和公开了底层持久性技术的功能CrudRepository

最重要的是CrudRepository,有一个PagingAndSortingRepository抽象添加了额外的方法来简化对实体的分页访问:例2. PagingAndSortingRepository界面

public interface PagingAndSortingRepository<T, ID extends Serializable>
  extends CrudRepository<T, ID> {

  Iterable<T> findAll(Sort sort);

  Page<T> findAll(Pageable pageable);
}

要访问User页面大小为20 的第二页,您可以执行以下操作:

PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));

除了查询方法之外,还可以使用计数和删除查询的查询派生。以下列表显示派生计数查询的接口定义:示例3.派生计数查询

interface UserRepository extends CrudRepository<User, Long> {

  long countByLastname(String lastname);
}

以下列表显示了派生删除查询的接口定义:示例4.派生的删除查询

interface UserRepository extends CrudRepository<User, Long> {

  long deleteByLastname(String lastname);

  List<User> removeByLastname(String lastname);
}

1.2 查询方法

标准CRUD功能存储库通常对基础数据存储区进行查询。使用Spring Data,声明这些查询将分为四个步骤:

  1. 声明扩展Repository或其子接口之一的接口,并将其键入应处理的域类和ID类型,如以下示例所示:interface PersonRepository extends Repository<Person, Long> { … }
  2. 在接口上声明查询方法。interface PersonRepository extends Repository<Person, Long> { List<Person> findByLastname(String lastname); }
  3. 设置Spring以使用JavaConfigXML配置为这些接口创建代理实例。
    1. 要使用Java配置,请创建类似于以下内容的类:import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @EnableJpaRepositories class Config {}
    2. 要使用XML配置,请定义类似于以下内容的bean:<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://www.springframework.org/schema/data/jpa" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/data/jpa https://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> <jpa:repositories base-package="com.acme.repositories"/> </beans>
    在此示例中使用JPA名称空间。如果对任何其他商店使用存储库抽象,则需要将其更改为商店模块的相应名称空间声明。换句话说,你应该换一个jpa赞成,例如,mongodb。+另外,请注意JavaConfig变体不会显式配置包,因为默认情况下使用带注释的类的包。要自定义要扫描的包,请使用basePackage…特定于数据存储库的@Enable${store}Repositories-annotation 的属性之一。
  4. 注入存储库实例并使用它,如以下示例所示:class SomeClient { private final PersonRepository repository; SomeClient(PersonRepository repository) { this.repository = repository; } void doSomething() { List<Person> persons = repository.findByLastname("Matthews"); } }

以下部分详细说明了每个步骤:

1.3。定义存储库接口

首先,定义特定于域类的存储库接口。接口必须扩展Repository并键入域类和ID类型。如果要为该域类型公开CRUD方法,请扩展CrudRepository而不是Repository

1.3.1。微调存储库定义

通常情况下,你的资料库接口扩展RepositoryCrudRepositoryPagingAndSortingRepository。或者,如果您不想扩展Spring Data接口,还可以使用注释来存储您的存储库接口@RepositoryDefinition。扩展CrudRepository公开了一整套方法来操纵您的实体。如果您希望对要公开的方法有选择性,请将要公开的方法复制CrudRepository到域存储库中。

这样做可以让您在提供的Spring Data Repositories功能之上定义自己的抽象。

下面的示例示出了如何以选择性地露出CRUD方法(findByIdsave,在这种情况下):示例5.有选择地暴露CRUD方法

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> {

  Optional<T> findById(ID id);

  <S extends T> S save(S entity);
}

interface UserRepository extends MyBaseRepository<User, Long> {
  User findByEmailAddress(EmailAddress emailAddress);
}

在前面的例子,你定义为所有站点库一个共同的基础界面和暴露findById(…),以及save(…)。这些方法被发送到基础信息库实现你所选择的由Spring提供的数据(例如,如果使用JPA商店,实现是SimpleJpaRepository),因为它们匹配方法签名CrudRepository。因此,UserRepository现在可以保存用户,按ID查找单个用户,并触发查询以Users通过电子邮件地址查找。

中间存储库接口使用注释@NoRepositoryBean。确保将该注释添加到Spring Data不应在运行时创建实例的所有存储库接口。

1.3.2。存储库方法的空处理

从Spring Data 2.0开始,返回单个聚合实例的存储库CRUD方法使用Java 8 Optional来指示可能缺少值。除此之外,Spring Data支持在查询方法上返回以下包装类型:

  • com.google.common.base.Optional
  • scala.Option
  • io.vavr.control.Option
  • javaslang.control.Option (不推荐使用Javaslang)

或者,查询方法可以选择根本不使用包装类型。然后通过返回指示缺少查询结果null。保证返回集合,集合替代,包装器和流的存储库方法永远不会返回null,而是相应的空表示。有关详细信息,请参阅“ 存储库查询返回类型 ”。

可空性注释

您可以使用Spring Framework的可空性注释来表达存储库方法的可空性约束。它们null在运行时提供了一种工具友好的方法和选择性检查,如下所示:

  • @NonNullApi:在包级别上使用,以声明参数和返回值的默认行为是不接受或生成null值。
  • @NonNull:用于不能是参数或返回值null (参数不需要,返回值@NonNullApi适用)。
  • @Nullable:用于可以的参数或返回值null

Spring注释是使用JSR 305注释进行元注释的(一种休眠但广泛传播的JSR)。JSR 305元注释允许IDEAEclipseKotlin等工具供应商以通用方式提供空安全支持,而无需对Spring注释进行硬编码支持。要为查询方法启用运行时检查可空性约束,需要使用Spring的@NonNullApiin 来激活包级别的非可空性package-info.java,如以下示例所示:例6.声明中的非可空性 package-info.java

@org.springframework.lang.NonNullApi
package com.acme;

一旦存在非null默认值,就会在运行时验证存储库查询方法调用的可空性约束。如果查询执行结果违反了定义的约束,则抛出异常。当方法返回null但声明为非可空时(默认情况下,存储库所在的包中定义了注释)会发生这种情况。如果您想再次选择加入可空的结果,请有选择地使用@Nullable单个方法。使用本节开头提到的结果包装器类型将继续按预期工作:空结果将转换为表示缺席的值。

以下示例显示了刚才描述的一些技术:示例7.使用不同的可空性约束

package com.acme;                                                       

import org.springframework.lang.Nullable;

interface UserRepository extends Repository<User, Long> {

  User getByEmailAddress(EmailAddress emailAddress);                    

  @Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress);          

  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); 
}
存储库驻留在我们已定义非空行为的包(或子包)中。
抛出一个EmptyResultDataAccessException在执行时不产生结果的查询。抛出一个IllegalArgumentExceptionemailAddress交给该方法null
null执行查询时不返回结果。也接受null作为的值emailAddress
Optional.empty()执行查询时不返回结果。抛出一个IllegalArgumentExceptionemailAddress交给该方法null
基于Kotlin的存储库中的可空性

Kotlin 对语言中的可空性约束进行了定义。Kotlin代码编译为字节码,它不通过方法签名表达可空性约束,而是通过编译元数据表达。确保kotlin-reflect在项目中包含JAR,以便对Kotlin的可空性约束进行内省。Spring Data存储库使用语言机制来定义这些约束以应用相同的运行时检查,如下所示:示例8.对Kotlin存储库使用可空性约束

interface UserRepository : Repository<User, String> {

  fun findByUsername(username: String): User     

  fun findByFirstname(firstname: String?): User? 
}
该方法将参数和结果都定义为非可空(Kotlin默认值)。Kotlin编译器拒绝传递null给该方法的方法调用。如果查询执行产生空结果,EmptyResultDataAccessException则抛出a。
该方法接受nullfirstname参数,并返回null,如果查询执行不产生结果。

1.3.3。使用具有多个Spring数据模块的存储库

在应用程序中使用唯一的Spring Data模块会使事情变得简单,因为定义范围内的所有存储库接口都绑定到Spring Data模块。有时,应用程序需要使用多个Spring Data模块。在这种情况下,存储库定义必须区分持久性技术。当它在类路径上检测到多个存储库工厂时,Spring Data进入严格的存储库配置模式。严格配置使用存储库或域类的详细信息来确定存储库定义的Spring Data模块绑定:

  1. 如果存储库定义扩展了特定于模块的存储库,那么它是特定Spring Data模块的有效候选者。
  2. 如果使用特定于模块的类型注释对域类进行注释,则它是特定Spring Data模块的有效候选者。Spring Data模块接受第三方注释(例如JPA @Entity)或提供自己的注释(例如@DocumentSpring Data MongoDB和Spring Data Elasticsearch)。

以下示例显示了使用特定于模块的接口的存储库(在本例中为JPA):示例9.使用模块特定接口的存储库定义

interface MyRepository extends JpaRepository<User, Long> { }

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
  …
}

interface UserRepository extends MyBaseRepository<User, Long> {
  …
}

MyRepository并在其类型层次结构中UserRepository扩展JpaRepository。它们是Spring Data JPA模块的有效候选者。

以下示例显示了使用通用接口的存储库:示例10.使用通用接口的存储库定义

interface AmbiguousRepository extends Repository<User, Long> {
 …
}

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {
  …
}

interface AmbiguousUserRepository extends MyBaseRepository<User, Long> {
  …
}

AmbiguousRepositoryAmbiguousUserRepository仅延伸Repository,并CrudRepository在他们的类型层次。虽然这在使用独特的Spring Data模块时非常好,但是多个模块无法区分这些存储库应该绑定到哪个特定的Spring Data。

以下示例显示了使用带注释的域类的存储库:示例11.使用带注释的域类的存储库定义

interface PersonRepository extends Repository<Person, Long> {
 …
}

@Entity
class Person {
  …
}

interface UserRepository extends Repository<User, Long> {
 …
}

@Document
class User {
  …
}

PersonRepository引用Person,使用JPA @Entity注释进行注释,因此该存储库显然属于Spring Data JPA。UserRepository引用User,使用Spring Data MongoDB的@Document注释进行注释。

以下错误示例显示了使用具有混合注释的域类的存储库:示例12.使用具有混合注释的域类的存储库定义

interface JpaPersonRepository extends Repository<Person, Long> {
 …
}

interface MongoDBPersonRepository extends Repository<Person, Long> {
 …
}

@Entity
@Document
class Person {
  …
}

此示例显示了使用JPA和Spring Data MongoDB注释的域类。它定义了两个存储库,JpaPersonRepositoryMongoDBPersonRepository。一个用于JPA,另一个用于MongoDB用法。Spring Data不再能够将存储库分开,从而导致未定义的行为。

存储库类型详细信息区分域类注释用于严格存储库配置,以识别特定Spring数据模块的存储库候选。在同一域类型上使用多个持久性技术特定的注释是可能的,并允许跨多种持久性技术重用域类型。但是,Spring Data不再能够确定用于绑定存储库的唯一模块。

区分存储库的最后一种方法是通过确定存储库基础包的范围。基础包定义了扫描存储库接口定义的起点,这意味着将存储库定义放在相应的包中。默认情况下,注释驱动的配置使用配置类的包。基于XML的配置中基础包是必需的。

以下示例显示了基本包的注释驱动配置:示例13.基础包的注释驱动配置

@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
interface Configuration { }

1.4。定义查询方法

存储库代理有两种方法可以从方法名称派生特定于商店的查询:

  • 通过直接从方法名称派生查询。
  • 通过使用手动定义的查询。

可用选项取决于实际商店。但是,必须有一个策略来决定创建实际查询的内容。下一节将介绍可用选项。

1.4.1。查询查找策略

存储库基础结构可以使用以下策略来解析查询。使用XML配置,您可以通过query-lookup-strategy属性在命名空间配置策略。对于Java配置,您可以使用注释的queryLookupStrategy属性Enable${store}Repositories。特定数据存储可能不支持某些策略。

  • CREATE尝试从查询方法名称构造特定于商店的查询。一般方法是从方法名称中删除一组已知的前缀,并解析方法的其余部分。您可以在“ 查询创建 ”中阅读有关查询构造的更多信息。
  • USE_DECLARED_QUERY尝试查找声明的查询,如果找不到,则抛出异常。查询可以通过某处的注释来定义,也可以通过其他方式声明。查阅特定商店的文档以查找该商店​​的可用选项。如果存储库基础结构在引导时未找到该方法的声明查询,则它将失败。
  • CREATE_IF_NOT_FOUND(默认)组合CREATEUSE_DECLARED_QUERY。它首先查找声明的查询,如果没有找到声明的查询,它会创建一个基于自定义方法名称的查询。这是默认的查找策略,因此,如果您未明确配置任何内容,则使用此策略。它允许通过方法名称进行快速查询定义,还可以根据需要引入声明的查询来自定义这些查询。

1.4.2。查询创建

Spring Data存储库基础结构中内置的查询构建器机制对于构建对存储库实体的约束查询非常有用。该机制条前缀find…Byread…Byquery…Bycount…By,和get…By从所述方法和开始分析它的其余部分。introduction子句可以包含更多表达式,例如Distinct在要创建的查询上设置不同的标志。但是,第一个By用作分隔符以指示实际标准的开始。在最基本的层面上,您可以在实体属性上定义条件,并将它们与And和它们连接起来Or。以下示例显示了如何创建大量查询:示例14.从方法名称创建查询

interface PersonRepository extends Repository<User, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

解析方法的实际结果取决于您为其创建查询的持久性存储。但是,有一些一般要注意的事项:

  • 表达式通常是属性遍历与可以连接的运算符相结合。您可以将属性表达式与AND和组合使用OR。您还可以得到这样的运营商为支撑BetweenLessThanGreaterThan,和Like该属性的表达式。支持的运算符可能因数据存储而异,因此请参阅参考文档的相应部分。
  • 方法解析器支持IgnoreCase为各个属性(例如findByLastnameIgnoreCase(…))或支持忽略大小写的类型(String例如,通常为实例)的所有属性设置标志findByLastnameAndFirstnameAllIgnoreCase(…)。是否支持忽略大小写可能因商店而异,因此请参阅参考文档中有关特定于商店的查询方法的相关章节。
  • 您可以通过OrderBy在引用属性的查询方法中附加子句并提供排序方向(AscDesc)来应用静态排序。要创建支持动态排序的查询方法,请参阅“ 特殊参数处理 ”。

1.4.3。财产表达

属性表达式只能引用被管实体的直接属性,如前面的示例所示。在创建查询时,您已确保已解析的属性是托管域类的属性。但是,您也可以通过遍历嵌套属性来定义约束。请考虑以下方法签名:

List<Person> findByAddressZipCode(ZipCode zipCode);

假设a PersonAddress一个ZipCode。在这种情况下,该方法创建属性遍历x.address.zipCode。解析算法首先将整个part(AddressZipCode)解释为属性,并检查域类中是否具有该名称的属性(未大写)。如果算法成功,它将使用该属性。如果没有,算法将来自右侧的驼峰部分的源分成头部和尾部,并尝试找到相应的属性 – 在我们的示例中,AddressZipCode。如果算法找到具有该头部的属性,则它采用尾部并继续从那里构建树,以刚刚描述的方式将尾部分开。如果先拆不匹配,则算法移动分割点左侧(AddressZipCode)并继续。

虽然这应该适用于大多数情况,但算法可能会选择错误的属性。假设Person该类也具有addressZip属性。算法将在第一个拆分轮中匹配,选择错误的属性,并且失败(因为类型addressZip可能没有code属性)。

要解决这种歧义,您可以_在方法名称中使用手动定义遍历点。所以我们的方法名称如下:

List<Person> findByAddress_ZipCode(ZipCode zipCode);

因为我们将下划线字符视为保留字符,所以我们强烈建议遵循标准Java命名约定(即,不在属性名称中使用下划线,而是使用camel case)。

1.4.4。特殊参数处理

要处理查询中的参数,请定义方法参数,如前面示例中所示。除此之外,基础设施承认某些特定的类型,如PageableSort,动态地应用分页和排序,以查询。以下示例演示了这些功能:示例15.使用Pageable,, SliceSort查询方法

Page<User> findByLastname(String lastname, Pageable pageable);

Slice<User> findByLastname(String lastname, Pageable pageable);

List<User> findByLastname(String lastname, Sort sort);

List<User> findByLastname(String lastname, Pageable pageable);

第一种方法允许您将org.springframework.data.domain.Pageable实例传递给查询方法,以动态地将分页添加到静态定义的查询中。A Page知道可用元素和页面的总数。它通过基础设施触发计数查询来计算总数来实现。由于这可能很昂贵(取决于所使用的商店),您可以改为返回Slice。A Slice只知道下一个Slice是否可用,这在遍历更大的结果集时可能就足够了。

排序选项也通过Pageable实例处理。如果只需要排序,org.springframework.data.domain.Sort请在方法中添加参数。如您所见,返回a List也是可能的。在这种情况下,Page不会创建构建实际实例所需的其他元数据(这反过来意味着不会发出必要的附加计数查询)。相反,它限制查询仅查找给定范围的实体。

要了解整个查询的页数,必须触发其他计数查询。默认情况下,此查询是从您实际触发的查询派生的。

1.4.5。限制查询结果

查询方法的结果可以通过使用firsttop关键字来限制,可以互换使用。可以附加一个可选的数值,top或者first指定要返回的最大结果大小。如果省略该数字,则假定结果大小为1。以下示例显示如何限制查询大小:示例16.使用Top和限制查询的结果大小First

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

限制表达式也支持Distinct关键字。此外,对于将结果集限制为一个实例的查询,支持将结果包装到Optional关键字中。

如果将分页或切片应用于限制查询分页(以及可用页数的计算),则将其应用于有限结果中。

通过使用Sort参数将结果与动态排序结合使用,可以表达“K”最小元素和“K”元素的查询方法。

1.4.6。流式查询结果

可以使用Java 8 Stream<T>作为返回类型以递增方式处理查询方法的结果。而不是将查询结果包装在Stream数据存储中的特定方法用于执行流式传输,如以下示例所示:示例17.使用Java 8流式传输查询结果 Stream<T>

@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);
Stream潜在包装底层数据存储专用资源,因此必须在使用之后被关闭。您可以Stream使用该close()方法或使用Java 7 try-with-resources块手动关闭,如以下示例所示:

示例18. Stream<T>在try-with-resources块中使用结果

try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}
并非所有Spring Data模块当前都支持Stream<T>返回类型。

1.4.7。异步查询结果

可以使用Spring的异步方法执行功能异步运行存储库查询。这意味着该方法在调用时立即返回,而实际的查询执行发生在已提交给Spring的任务中TaskExecutor。异步查询执行与反应式查询执行不同,不应混合使用。有关反应支持的更多详细信息,请参阅特定于商店的文档。以下示例显示了许多异步查询:

@Async
Future<User> findByFirstname(String firstname);               

@Async
CompletableFuture<User> findOneByFirstname(String firstname); 

@Async
ListenableFuture<User> findOneByLastname(String lastname);    
使用java.util.concurrent.Future作为返回类型。
使用Java 8 java.util.concurrent.CompletableFuture作为返回类型。
使用a org.springframework.util.concurrent.ListenableFuture作为返回类型。

1.5。创建存储库实例

在本节中,您将为定义的存储库接口创建实例和bean定义。一种方法是使用随每个支持存储库机制的Spring Data模块一起提供的Spring命名空间,尽管我们通常建议使用Java配置。

1.5.1。XML配置

每个Spring Data模块都包含一个repositories元素,允许您定义Spring为您扫描的基础包,如以下示例所示:示例19.通过XML启用Spring Data存储库

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://www.springframework.org/schema/data/jpa"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/jpa
    https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

  <repositories base-package="com.acme.repositories" />

</beans:beans>

在前面的示例中,指示Spring扫描com.acme.repositories其所有子包以进行扩展Repository或其子接口之一。对于找到的每个接口,基础结构都会注册特定FactoryBean于持久性技术的内容,以创建处理查询方法调用的相应代理。每个bean都是在从接口名称派生的bean名称下注册的,因此UserRepository将在其下注册一个接口userRepository。该base-package属性允许使用通配符,以便您可以定义扫描包的模式。

使用过滤器

默认情况下,基础结构会选择扩展Repository位于已配置的基本包下的特定于持久性技术的子接口的每个接口,并为其创建一个bean实例。但是,您可能希望对哪些接口为其创建bean实例进行更细粒度的控制。要做到这一点,使用<include-filter /><exclude-filter />内部元素<repositories />的元素。语义完全等同于Spring的上下文命名空间中的元素。有关详细信息,请参阅这些元素的Spring参考文档

例如,要将某些接口从实例化中排除为存储库bean,可以使用以下配置:示例20.使用exclude-filter元素

<repositories base-package="com.acme.repositories">
  <context:exclude-filter type="regex" expression=".*SomeRepository" />
</repositories>

前面的示例排除了SomeRepository从实例化结束的所有接口。

1.5.2。JavaConfig

还可以通过@Enable${store}Repositories在JavaConfig类上使用特定于商店的注释来触发存储库基础结构。有关Spring容器的基于Java的配置的介绍,请参阅Spring参考文档中的JavaConfig

启用S​​pring Data存储库的示例配置类似于以下内容:示例21.基于样本注释的存储库配置

@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {

  @Bean
  EntityManagerFactory entityManagerFactory() {
    // …
  }
}
上面的示例使用JPA特定的注释,您可以根据实际使用的商店模块进行更改。这同样适用于EntityManagerFactorybean的定义。请参阅涵盖特定于商店的配置的部分。

1.5.3。独立使用

您还可以在Spring容器之外使用存储库基础结构 – 例如,在CDI环境中。您仍然需要在类路径中使用一些Spring库,但通常也可以通过编程方式设置存储库。提供存储库支持的Spring Data模块提供了特定RepositoryFactory于持久性技术的特性,您可以按如下方式使用:示例22.存储库工厂的独立使用

RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);

1.6。Spring数据存储库的自定义实现

本节介绍存储库自定义以及片段如何构成复合存储库。

当查询方法需要不同的行为或无法通过查询派生实现时,则需要提供自定义实现。Spring Data存储库允许您提供自定义存储库代码,并将其与通用CRUD抽象和查询方法功能集成。

1.6.1。自定义单个存储库

要使用自定义功能丰富存储库,必须首先定义片段接口和自定义功能的实现,如以下示例所示:示例23.自定义存储库功能的接口

interface CustomizedUserRepository {
  void someCustomMethod(User user);
}

然后,您可以让您的存储库接口从片段接口进一步扩展,如以下示例所示:示例24.自定义存储库功能的实现

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  public void someCustomMethod(User user) {
    // Your custom implementation
  }
}
对应于片段接口的类名最重要的部分是Impl后缀。

实现本身不依赖于Spring Data,可以是常规的Spring bean。因此,您可以使用标准依赖项注入行为将引用注入其他bean(例如a JdbcTemplate),参与方面等。

您可以让存储库接口扩展片段接口,如以下示例所示:示例25.对存储库界面的更改

interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {

  // Declare query methods here
}

使用存储库接口扩展片段接口可以组合CRUD和自定义功能,并使其可供客户端使用。

Spring Data存储库通过使用形成存储库组合的片段来实现。片段是基本存储库,功能方面(如QueryDsl),自定义接口及其实现。每次向存储库界面添加接口时,都可以通过添加片段来增强组合。每个Spring Data模块都提供了基本存储库和存储库方面的实现。

以下示例显示了自定义接口及其实现:示例26.具有其实现的片段

interface HumanRepository {
  void someHumanMethod(User user);
}

class HumanRepositoryImpl implements HumanRepository {

  public void someHumanMethod(User user) {
    // Your custom implementation
  }
}

interface ContactRepository {

  void someContactMethod(User user);

  User anotherContactMethod(User user);
}

class ContactRepositoryImpl implements ContactRepository {

  public void someContactMethod(User user) {
    // Your custom implementation
  }

  public User anotherContactMethod(User user) {
    // Your custom implementation
  }
}

以下示例显示了扩展的自定义存储库的接口CrudRepository:示例27.对存储库界面的更改

interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

  // Declare query methods here
}

存储库可以由多个自定义实现组成,这些实现按其声明的顺序导入。自定义实现的优先级高于基本实现和存储库方面。如果两个片段提供相同的方法签名,则此排序允许您覆盖基本存储库和方面方法并解决歧义。存储库片段不限于在单个存储库接口中使用。多个存储库可以使用片段接口,允许您跨不同的存储库重用自定义。

以下示例显示了存储库片段及其实现:示例28.碎片覆盖 save(…)

interface CustomizedSave<T> {
  <S extends T> S save(S entity);
}

class CustomizedSaveImpl<T> implements CustomizedSave<T> {

  public <S extends T> S save(S entity) {
    // Your custom implementation
  }
}

以下示例显示了使用前面的存储库片段的存储库:示例29.定制的存储库接口

interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}

interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}
组态

如果使用命名空间配置,则存储库基础结构会尝试通过扫描其找到存储库的包下面的类来自动检测自定义实现片段。这些类需要遵循将namespace元素的repository-impl-postfix属性附加到片段接口名称的命名约定。此后缀默认为Impl。以下示例显示了使用默认后缀的存储库以及为后缀设置自定义值的存储库:示例30.配置示例

<repositories base-package="com.acme.repository" />

<repositories base-package="com.acme.repository" repository-impl-postfix="MyPostfix" />

上一个示例中的第一个配置尝试查找一个名为“ com.acme.repository.CustomizedUserRepositoryImpl自定义存储库实现”的类。第二个示例尝试查找com.acme.repository.CustomizedUserRepositoryMyPostfix

解决歧义

如果在不同的包中找到具有匹配类名的多个实现,则Spring Data使用bean名称来标识要使用的名称。

给定CustomizedUserRepository前面所示的以下两个自定义实现,使用第一个实现。它的bean名称customizedUserRepositoryImpl与片段interface(CustomizedUserRepository)和postfix的名称相匹配Impl。示例31.无差别实现的解决方案

package com.acme.impl.one;

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}
package com.acme.impl.two;

@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}

如果使用bean 注释UserRepository接口@Component("specialCustom"),则bean名称加上Impl与为存储库实现定义的名称相匹配com.acme.impl.two,并使用它而不是第一个。

手动接线

如果您的自定义实现仅使用基于注释的配置和自动装配,则前面显示的方法效果很好,因为它被视为任何其他Spring bean。如果您的实现片段bean需要特殊连接,您可以声明bean并根据前一节中描述的约定命名它。然后,基础结构按名称引用手动定义的bean定义,而不是自己创建一个。以下示例显示如何手动连接自定义实现:示例32.自定义实现的手动连接

<repositories base-package="com.acme.repository" />

<beans:bean id="userRepositoryImpl" class="…">
  <!-- further configuration -->
</beans:bean>

1.6.2。自定义Base Repository

当您要自定义基本存储库行为以便所有存储库都受到影响时,上一节中描述的方法需要自定义每个存储库接口。要改为更改所有存储库的行为,可以创建一个扩展特定于持久性技术的存储库基类的实现。然后,此类充当存储库代理的自定义基类,如以下示例所示:示例33.自定义存储库基类

class MyRepositoryImpl<T, ID extends Serializable>
  extends SimpleJpaRepository<T, ID> {

  private final EntityManager entityManager;

  MyRepositoryImpl(JpaEntityInformation entityInformation,
                          EntityManager entityManager) {
    super(entityInformation, entityManager);

    // Keep the EntityManager around to used from the newly introduced methods.
    this.entityManager = entityManager;
  }

  @Transactional
  public <S extends T> S save(S entity) {
    // implementation goes here
  }
}
该类需要具有特定于商店的存储库工厂实现所使用的超类的构造函数。如果存储库基类具有多个构造函数,则覆盖使用EntityInformation加号存储特定基础结构对象(例如,EntityManager模板类)的构造函数。

最后一步是使Spring Data Infrastructure了解自定义存储库基类。在Java配置中,您可以使用注释的repositoryBaseClass属性来执行此操作@Enable${store}Repositories,如以下示例所示:示例34.使用JavaConfig配置自定义存储库基类

@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }

XML命名空间中提供了相应的属性,如以下示例所示:示例35.使用XML配置自定义存储库基类

<repositories base-package="com.acme.repository"
     base-class="….MyRepositoryImpl" />

1.7。从聚合根发布事件

由存储库管理的实体是聚合根。在域驱动设计应用程序中,这些聚合根通常会发布域事件。Spring Data提供了一个注释@DomainEvents,可以在聚合根的方法上使用,以使该发布尽可能简单,如以下示例所示:示例36.从聚合根公开事件域事件

class AnAggregateRoot {

    @DomainEvents 
    Collection<Object> domainEvents() {
        // … return events you want to get published here
    }

    @AfterDomainEventPublication 
    void callbackMethod() {
       // … potentially clean up domain events list
    }
}
using方法@DomainEvents可以返回单个事件实例或事件集合。它不能采取任何论点。
在所有事件发布后,我们有一个注释的方法@AfterDomainEventPublication。它可用于潜在地清除要发布的事件列表(以及其他用途)。

每次调用Spring Data存储库的save(…)方法时,都会调用这些方法。

1.8。Spring Data Extensions

本节介绍了一组Spring Data扩展,它们可以在各种上下文中使用Spring Data。目前,大多数集成都针对Spring MVC。

1.8.1。Querydsl扩展

Querydsl是一个框架,可以通过其流畅的API构建静态类型的SQL类查询。

多个Spring Data模块提供与Querydsl的集成QuerydslPredicateExecutor,如以下示例所示:示例37. QuerydslPredicateExecutor接口

public interface QuerydslPredicateExecutor<T> {

  Optional<T> findById(Predicate predicate);  

  Iterable<T> findAll(Predicate predicate);   

  long count(Predicate predicate);            

  boolean exists(Predicate predicate);        

  // … more functionality omitted.
}
查找并返回与之匹配的单个实体Predicate
查找并返回与之匹配的所有实体Predicate
返回匹配的实体数Predicate
返回与Predicateexists 匹配的实体。

要使用Querydsl支持,请QuerydslPredicateExecutor在存储库界面上进行扩展,如以下示例所示示例38.存储库上的Querydsl集成

interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

上面的示例允许您使用Querydsl Predicate实例编写类型安全查询,如以下示例所示:

Predicate predicate = user.firstname.equalsIgnoreCase("dave")
	.and(user.lastname.startsWithIgnoreCase("mathews"));

userRepository.findAll(predicate);

1.8.2。网络支持

本节包含Spring Data Web支持的文档,因为它在Spring Data Commons的当前(及更高版本)版本中实现。由于新引入的支持更改了许多内容,因此我们在[web.legacy]中保留了以前行为的文档。

支持存储库编程模型的Spring Data模块具有各种Web支持。Web相关组件要求Spring MVC JAR位于类路径上。其中一些甚至提供与Spring HATEOAS的集成。通常,通过@EnableSpringDataWebSupport在JavaConfig配置类中使用注释来启用集成支持,如以下示例所示:示例39.启用Spring Data Web支持

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}

@EnableSpringDataWebSupport批注注册几个组件,我们将在一个位讨论。它还将检测类路径上的Spring HATEOAS,并为它注册集成组件(如果存在)。

或者,如果您使用XML配置,请注册SpringDataWebConfigurationHateoasAwareSpringDataWebConfiguration作为Spring bean 注册,如以下示例所示(for SpringDataWebConfiguration):示例40.在XML中启用Spring Data Web支持

<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />

<!-- If you use Spring HATEOAS, register this one *instead* of the former -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />
基本Web支持

上一节中显示的配置注册了一些基本组件:

DomainClassConverter

DomainClassConverter让你在Spring MVC中的控制器方法签名使用域类型直接,让你不用通过储存手动查找的情况下,如在下面的例子:示例41.在方法签名中使用域类型的Spring MVC控制器

@Controller
@RequestMapping("/users")
class UserController {

  @RequestMapping("/{id}")
  String showUserForm(@PathVariable("id") User user, Model model) {

    model.addAttribute("user", user);
    return "userForm";
  }
}

如您所见,该方法User直接接收实例,无需进一步查找。可以通过让Spring MVC首先将路径变量转换为id域类的类型来解析实例,并最终通过调用findById(…)为域类型注册的存储库实例来访问实例。

目前,存储库必须实现CrudRepository才有资格被发现进行转换。
HandlerMethodArgumentResolvers for Pageable和Sort

上一节中显示的配置代码还注册了一个PageableHandlerMethodArgumentResolver以及一个实例SortHandlerMethodArgumentResolver。注册启用PageableSort作为有效的控制器方法参数,如以下示例所示:示例42.使用Pageable作为控制器方法参数

@Controller
@RequestMapping("/users")
class UserController {

  private final UserRepository repository;

  UserController(UserRepository repository) {
    this.repository = repository;
  }

  @RequestMapping
  String showUsers(Model model, Pageable pageable) {

    model.addAttribute("users", repository.findAll(pageable));
    return "users";
  }
}

上述方法签名导致Spring MVC尝试Pageable使用以下默认配置从请求参数派生实例:

page您要检索的页面。0索引,默认为0。
size要检索的页面大小。默认为20。
sort应按格式排序的属性property,property(,ASC|DESC)。默认排序方向是升序。sort如果要切换方向,请使用多个参数 – 例如,?sort=firstname&sort=lastname,asc

要自定义此行为,请分别注册实现PageableHandlerMethodArgumentResolverCustomizer接口或SortHandlerMethodArgumentResolverCustomizer接口的Bean 。customize()调用其方法,让您更改设置,如以下示例所示:

@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
    return s -> s.setPropertyDelimiter("<-->");
}

如果设置现有属性MethodArgumentResolver不足以满足您的需要,请扩展其中一个SpringDataWebConfiguration或启用HATEOAS的等效项,重写pageableResolver()sortResolver()方法,然后导入自定义配置文件而不是使用@Enable注释。

如果您需要从请求中解析多个PageableSort实例(例如,对于多个表),则可以使用Spring的@Qualifier注释来区分彼此。然后请求参数必须以前缀为前缀${qualifier}_。以下示例显示了生成的方法签名:

String showUsers(Model model,
      @Qualifier("thing1") Pageable first,
      @Qualifier("thing2") Pageable second) { … }

你有填充thing1_pagething2_page等。

Pageable传递给方法的默认值相当于a,PageRequest.of(0, 20)但可以使用参数@PageableDefault上的注释进行自定义Pageable

对Pageables的超媒体支持

Spring HATEOAS附带了一个表示模型类(PagedResources),它允许Page使用必要的Page元数据丰富实例的内容,以及让客户端轻松浏览页面的链接。将Page转换为a PagedResources是由Spring HATEOAS ResourceAssembler接口的实现完成的,称为PagedResourcesAssembler。以下示例显示如何使用a PagedResourcesAssembler作为控制器方法参数:示例43.使用PagedResourcesAssembler作为控制器方法参数

@Controller
class PersonController {

  @Autowired PersonRepository repository;

  @RequestMapping(value = "/persons", method = RequestMethod.GET)
  HttpEntity<PagedResources<Person>> persons(Pageable pageable,
    PagedResourcesAssembler assembler) {

    Page<Person> persons = repository.findAll(pageable);
    return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
  }
}

如上例所示启用配置可以将PagedResourcesAssembler其用作控制器方法参数。调用toResources(…)它具有以下效果:

  • 内容Page成为PagedResources实例的内容。
  • PagedResources对象获得一个PageMetadata附加实例,并使用来自Page和底层的信息填充PageRequest
  • PagedResources可能会prevnext连接链路,根据页面的状态。链接指向方法映射到的URI。添加到方法的分页参数与设置相匹配,PageableHandlerMethodArgumentResolver以确保稍后可以解析链接。

假设我们在数据库中有30个Person实例。您现在可以触发request()并查看类似于以下内容的输出:GEThttp://localhost:8080/persons

{ "links" : [ { "rel" : "next",
                "href" : "http://localhost:8080/persons?page=1&size=20 }
  ],
  "content" : [
     … // 20 Person instances rendered here
  ],
  "pageMetadata" : {
    "size" : 20,
    "totalElements" : 30,
    "totalPages" : 2,
    "number" : 0
  }
}

您会看到汇编程序生成了正确的URI,并且还选择了默认配置以将参数解析Pageable为即将发出的请求。这意味着,如果更改该配置,链接将自动遵循更改。默认情况下,汇编程序指向它所调用的控制器方法,但是可以通过交换自定义来自定义Link以构建分页链接,从而使PagedResourcesAssembler.toResource(…)方法重载。

Web数据绑定支持

Spring Data投影(在[projection]中描述)可以通过使用JSONPath表达式来绑定传入的请求有效负载(需要Jayway JsonPathXPath表达式(需要XmlBeam)),如以下示例所示:示例44.使用JSONPath或XPath表达式的HTTP有效负载绑定

@ProjectedPayload
public interface UserPayload {

  @XBRead("//firstname")
  @JsonPath("$..firstname")
  String getFirstname();

  @XBRead("/lastname")
  @JsonPath({ "$.lastname", "$.user.lastname" })
  String getLastname();
}

前面示例中显示的类型可以用作Spring MVC处理程序方法参数,也可以使用ParameterizedTypeReference其中一个RestTemplate方法。前面的方法声明将尝试查找firstname给定文档中的任何位置。该lastnameXML查询是对输入文档的顶层进行。JSON变体lastname首先尝试顶级,但如果前者没有返回值,也会尝试lastname嵌套在user子文档中。这样,可以轻松地减轻源文档结构的变化,而无需客户端调用公开的方法(通常是基于类的有效负载绑定的缺点)。

[投影]中所述,支持嵌套投影。如果该方法返回复杂的非接口类型,ObjectMapper则使用Jackson 来映射最终值。

对于Spring MVC,必要的转换器会@EnableSpringDataWebSupport在活动时自动注册,并且类路径上可以使用所需的依赖项。使用时RestTemplate,注册ProjectingJackson2HttpMessageConverter(JSON)或XmlBeamHttpMessageConverter手动。

有关更多信息,请参阅规范Spring Data Examples存储库中Web投影示例

Querydsl Web支持

对于那些具有QueryDSL集成的商店,可以从Request查询字符串中包含的属性派生查询。

请考虑以下查询字符串:

?firstname=Dave&lastname=Matthews

给定User前面示例中的对象,可以使用以下方法将查询字符串解析为以下值QuerydslPredicateArgumentResolver

QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
@EnableSpringDataWebSupport当在类路径中找到Querydsl时 ,该功能会自动启用。

添加@QuerydslPredicate到方法签名提供了一个即用型Predicate,可以使用该方法运行QuerydslPredicateExecutor

通常从方法的返回类型中解析类型信息。由于该信息不一定与域类型匹配,因此使用该root属性可能是个好主意QuerydslPredicate

以下示例显示了如何@QuerydslPredicate在方法签名中使用:

@Controller
class UserController {

  @Autowired UserRepository repository;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,    
          Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {

    model.addAttribute("users", repository.findAll(predicate, pageable));

    return "index";
  }
}
将查询字符串参数解析为匹配Predicatefor User

默认绑定如下:

  • Object在简单的属性上eq
  • Object像集合一样的属性contains
  • Collection在简单的属性上in

可以通过Java 8 的bindings属性@QuerydslPredicate或通过使用Java 8 default methods并将该QuerydslBinderCustomizer方法添加到存储库接口来定制这些绑定。

interface UserRepository extends CrudRepository<User, String>,
                                 QuerydslPredicateExecutor<User>,                
                                 QuerydslBinderCustomizer<QUser> {               

  @Override
  default void customize(QuerydslBindings bindings, QUser user) {

    bindings.bind(user.username).first((path, value) -> path.contains(value))    
    bindings.bind(String.class)
      .first((StringPath path, String value) -> path.containsIgnoreCase(value)); 
    bindings.excluding(user.password);                                           
  }
}
QuerydslPredicateExecutor提供对特定查找程序方法的访问权限Predicate
QuerydslBinderCustomizer在存储库界面上定义的是自动拾取和快捷方式@QuerydslPredicate(bindings=…​)
username属性的绑定定义为简单contains绑定。
String属性的默认绑定定义为不区分大小写的contains匹配。
passwordPredicate解决方案中排除该属性。

1.8.3。存储库填充程序

如果您使用Spring JDBC模块,您可能熟悉DataSource使用SQL脚本填充的支持。虽然它不使用SQL作为数据定义语言,但它在存储库级别上可以使用类似的抽象,因为它必须与存储无关。因此,填充程序支持XML(通过Spring的OXM抽象)和JSON(通过Jackson)来定义用于填充存储库的数据。

假设您有一个data.json包含以下内容的文件:示例45.在JSON中定义的数据

[ { "_class" : "com.acme.Person",
 "firstname" : "Dave",
  "lastname" : "Matthews" },
  { "_class" : "com.acme.Person",
 "firstname" : "Carter",
  "lastname" : "Beauford" } ]

您可以使用Spring Data Commons中提供的存储库命名空间的populator元素来填充存储库。要将前面的数据填充到PersonRepository,请声明类似于以下内容的populator:示例46.声明Jackson存储库填充程序

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    https://www.springframework.org/schema/data/repository/spring-repository.xsd">

  <repository:jackson2-populator locations="classpath:data.json" />

</beans>

前面的声明导致data.json文件被Jackson读取和反序列化ObjectMapper

通过检查_classJSON文档的属性来确定解组JSON对象的类型。基础结构最终选择适当的存储库来处理反序列化的对象。

要使用XML来定义应该填充存储库的数据,您可以使用该unmarshaller-populator元素。您将其配置为使用Spring OXM中提供的XML marshaller选项之一。有关详细信息,请参阅Spring参考文档。以下示例说明如何使用JAXB解组存储库填充程序:示例47.声明一个解组存储库populator(使用JAXB)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xmlns:oxm="http://www.springframework.org/schema/oxm"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    https://www.springframework.org/schema/data/repository/spring-repository.xsd
    http://www.springframework.org/schema/oxm
    https://www.springframework.org/schema/oxm/spring-oxm.xsd">

  <repository:unmarshaller-populator locations="classpath:data.json"
    unmarshaller-ref="unmarshaller" />

  <oxm:jaxb2-marshaller contextPath="com.acme" />

</beans>

评论

还没有任何评论,你来说两句吧

你必须 登录 才能发表评论.