Spring-Boot-持久化最佳实践-八-

41 阅读44分钟

Spring Boot 持久化最佳实践(八)

原文:Spring Boot Persistence Best Practices

协议:CC BY-NC-SA 4.0

十五、继承

第 138 项:如何有效地使用单表继承

单表继承是默认的 JPA 策略。按照这种策略,继承层次结构中的所有类都通过数据库中的单个表来表示。

考虑图 15-1 中给出的继承层次。

img/487471_1_En_15_Fig1_HTML.jpg

图 15-1

单表继承域模型

AuthorBook之间有一个双向的懒惰@OneToMany关联。Author实体可以被视为根类,因为没有作者就没有书。Book实体是基类。为了采用单表继承策略,这个类用@Inheritance@Inheritance(strategy = InheritanceType.SINGLE_TABLE)进行了注释。EbookPaperback实体扩展了Book实体;所以,他们不需要自己的@Id

形成这种继承策略的表格如图 15-2 所示。

img/487471_1_En_15_Fig2_HTML.jpg

图 15-2

单表继承策略的表

book表包含与Book实体以及EbookPaperback实体相关联的列。它还包含一个名为dtype的栏目。这被称为鉴别器列。Hibernate 使用这个列将结果集映射到相关的子类实例。默认情况下,鉴别器列保存实体的名称。

如果您必须对一个遗留数据库使用SINGLE_TABLE策略,那么很可能您将没有一个鉴别器列,并且您不能改变表定义。在这种情况下,您可以使用@DiscriminatorFormula来定义一个公式(一个派生值)作为继承鉴别器列。一旦你知道了@DiscriminatorFormula,你可以很容易地在网上找到例子。

这里列出了Book基类及其子类的相关代码:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Book implements Serializable {
    ...
}

@Entity
public class Ebook extends Book implements Serializable {
    ...
}

@Entity
public class Paperback extends Book implements Serializable {
    ...
}

持久数据

是时候持久化一些数据了。下面的服务方法持久化一个包含三本书的Author,这三本书是通过BookEbookPaperback实体创建的:

public void persistAuthorWithBooks() {

    Author author = new Author();
    author.setName("Alicia Tom");
    author.setAge(38);
    author.setGenre("Anthology");

    Book book = new Book();
    book.setIsbn("001-AT");
    book.setTitle("The book of swords");

    Paperback paperback = new Paperback();
    paperback.setIsbn("002-AT");
    paperback.setTitle("The beatles anthology");
    paperback.setSizeIn("7.5 x 1.3 x 9.2");
    paperback.setWeightLbs("2.7");

    Ebook ebook = new Ebook();
    ebook.setIsbn("003-AT");
    ebook.setTitle("Anthology myths");
    ebook.setFormat("kindle");

    author.addBook(book); // use addBook() helper
    author.addBook(paperback);
    author.addBook(ebook);

    authorRepository.save(author);
}

保存author实例会触发以下 SQL 语句:

INSERT INTO author (age, genre, name)
  VALUES (?, ?, ?)
Binding:[38, Anthology, Alicia Tom]

INSERT INTO book (author_id, isbn, title, dtype)
  VALUES (?, ?, ?, 'Book')
Binding:[1, 001-AT, The book of swords]

INSERT INTO book (author_id, isbn, title, size_in, weight_lbs, dtype)
  VALUES (?, ?, ?, ?, ?, 'Paperback')
Binding:[1, 002-AT, The beatles anthology, 7.5 x 1.3 x 9.2, 2.7]

INSERT INTO book (author_id, isbn, title, format, dtype)
  VALUES (?, ?, ?, ?, 'Ebook')
Binding:[1, 003-AT, Anthology myths, kindle]

作者保存在author表中,而书籍(bookebookpaperback)保存在book表中。因此,持久化(写)数据是有效的,因为所有的书都保存在同一个表中。

查询和单表继承

现在,让我们来看看获取数据的效率。考虑以下BookRepository:

@Repository
@Transactional(readOnly = true)
public interface BookRepository extends JpaRepository<Book, Long> {

    @Query("SELECT b FROM Book b WHERE b.author.id = ?1")
    List<Book> fetchBooksByAuthorId(Long authorId);

    Book findByTitle(String title);
}

通过作者标识符获取图书

让我们打电话给fetchBooksByAuthorId():

List<Book> books = bookRepository.fetchBooksByAuthorId(1L);

触发的SELECT如下:

SELECT
  book0_.id AS id2_1_,
  book0_.author_id AS author_i8_1_,
  book0_.isbn AS isbn3_1_,
  book0_.title AS title4_1_,
  book0_.format AS format5_1_,
  book0_.size_in AS size_in6_1_,
  book0_.weight_lbs AS weight_l7_1_,
  book0_.dtype AS dtype1_1_
FROM book book0_
WHERE book0_.author_id = ?

继承为多态查询提供了支持。换句话说,获取的结果集被正确地映射到基类(Book)和子类(EbookPaperback)。Hibernate 通过检查每个获取的行的鉴别器列来做到这一点。

按书名取书

更进一步,让我们为每本书调用findByTitle():

Book b1 = bookRepository.findByTitle("The book of swords");    // Book
Book b2 = bookRepository.findByTitle("The beatles anthology"); // Paperback
Book b3 = bookRepository.findByTitle("Anthology myths");       // Ebook

触发的SELECT对于所有三种类型的图书都是相同的:

SELECT
  book0_.id AS id2_1_,
  book0_.author_id AS author_i8_1_,
  book0_.isbn AS isbn3_1_,
  book0_.title AS title4_1_,
  book0_.format AS format5_1_,
  book0_.size_in AS size_in6_1_,
  book0_.weight_lbs AS weight_l7_1_,
  book0_.dtype AS dtype1_1_
FROM book book0_
WHERE book0_.title = ?

获取b1b2b3作为Book实例不会混淆 Hibernate。由于b2是一个Paperback,它可以被显式强制转换以显示大小和重量:

Paperback p = (Paperback) b2;
System.out.println(p.getSizeIn());
System.out.println(p.getWeightLbs());

当然,这不像依赖子类的专用库那样实际。注意,我们在BookRepository中定义了findByTitle()。如果我们想从EbookRepositoryPaperbackRepository中使用它,那么复制它是不实际的(一般来说,在所有存储库中复制查询方法是不实际的)。在这种情况下,首先在@NoRepositoryBean类中定义findByTitle():

@NoRepositoryBean
public interface BookBaseRepository<T extends Book>
                            extends JpaRepository<T, Long> {
    T findByTitle(String title);
    @Query(value="SELECT b FROM #{#entityName} AS b WHERE b.isbn = ?1")

    T fetchByIsbn(String isbn);
}

接下来,BookRepositoryEbookRepositoryPaperbackRepository延伸BookBaseRepository。通过这种方式,findByTitle()findByIsbn()可以在所有扩展基本存储库的存储库中使用。完整的应用可在 GitHub 1 上获得。

去拿平装书

考虑下面列出的Paperback存储库:

@Repository
@Transactional(readOnly = true)
public interface PaperbackRepository    extends JpaRepository<Paperback, Long> {

    Paperback findByTitle(String title);
}

现在,让我们触发两个查询。第一个查询使用标识一个Book的标题。第二个查询使用标识一个Paperback的标题:

// this is a Book
Paperback p1 = paperbackRepository.findByTitle("The book of swords");

// this is a Paperback
Paperback p2 = paperbackRepository.findByTitle("The beatles anthology");

两个查询触发相同的SELECT:

SELECT
  paperback0_.id AS id2_1_,
  paperback0_.author_id AS author_i8_1_,
  paperback0_.isbn AS isbn3_1_,
  paperback0_.title AS title4_1_,
  paperback0_.size_in AS size_in6_1_,
  paperback0_.weight_lbs AS weight_l7_1_
FROM book paperback0_
WHERE paperback0_.dtype = 'Paperback'
AND paperback0_.title = ?

注意WHERE子句。Hibernate 附加了一个基于dtype的条件,只获取平装书;因此,p1将是null,而p2将是Paperback实例。太酷了,对吧?!

获取作者和相关书籍

考虑下面的Author存储库:

@Repository
@Transactional(readOnly = true)
public interface AuthorRepository extends JpaRepository<Author, Long> {

    Author findByName(String name);

    @Query("SELECT a FROM Author a JOIN FETCH a.books b")
    public Author findAuthor();
}

调用findByName()将获取没有相关书籍的作者:

@Transactional(readOnly = true)
public void fetchAuthorAndBooksLazy() {
    Author author = authorRepository.findByName("Alicia Tom");
    List<Book> books = author.getBooks();
}

调用getBooks()如预期的那样触发了第二个查询:

-- fetch the author
SELECT
  author0_.id AS id1_0_,
  author0_.age AS age2_0_,
  author0_.genre AS genre3_0_,
  author0_.name AS name4_0_
FROM author author0_
WHERE author0_.name = ?

-- fetch the books via getBooks()
SELECT
  books0_.author_id AS author_i8_1_0_,
  books0_.id AS id2_1_0_,
  books0_.id AS id2_1_1_,
  books0_.author_id AS author_i8_1_1_,
  books0_.isbn AS isbn3_1_1_,
  books0_.title AS title4_1_1_,
  books0_.format AS format5_1_1_,
  books0_.size_in AS size_in6_1_1_,
  books0_.weight_lbs AS weight_l7_1_1_,
  books0_.dtype AS dtype1_1_1_
FROM book books0_
WHERE books0_.author_id = ?

这正是预期的行为。

另一方面,由于有了JOIN FETCH,调用findAuthor()将在同一个SELECT中获取作者和相关书籍:

@Transactional(readOnly = true)
public void fetchAuthorAndBooksEager() {
    Author author = authorRepository.findAuthor();
}

被触发的SELECT依赖于INNER JOIN如下:

SELECT
  author0_.id AS id1_0_0_,
  books1_.id AS id2_1_1_,
  author0_.age AS age2_0_0_,
  author0_.genre AS genre3_0_0_,
  author0_.name AS name4_0_0_,
  books1_.author_id AS author_i8_1_1_,
  books1_.isbn AS isbn3_1_1_,
  books1_.title AS title4_1_1_,
  books1_.format AS format5_1_1_,
  books1_.size_in AS size_in6_1_1_,
  books1_.weight_lbs AS weight_l7_1_1_,
  books1_.dtype AS dtype1_1_1_,
  books1_.author_id AS author_i8_1_0__,
  books1_.id AS id2_1_0__
FROM author author0_
INNER JOIN book books1_
  ON author0_.id = books1_.author_id

很好!看起来单表继承支持快速读写。

子类属性非空性问题

支持在基类 ( Book)上指定不可为空的约束,这很简单,如下例所示:

public class Book implements Serializable {
    ...
    @Column(nullable=false)
    private String title;
    ...
}

试图持久化Book将导致类型SQLIntegrityConstraintViolationException: Column 'title' cannot be null:的预期异常

Book book = new Book();
book.setIsbn("001-AT");
book.setTitle(null);

但是试图在Book的子类上添加不可空的约束是不允许的。换句话说,不可能向属于EbookPaperback的列添加NOT NULL约束。这意味着下面的Ebook被成功持久化:

Ebook ebook = new Ebook();
ebook.setIsbn("003-AT");
ebook.setTitle("Anthology myths");
ebook.setFormat(null);

显然,将format设置为null违背了创建这个Ebook的目的。所以,创建一个Ebook不应该接受formatnull。以同样的方式,创建一个Paperback不应该接受nullsizeInweightLbs

然而,有几种解决方案可以确保子类属性的非空性。首先,在域模型上,依靠javax.validation.constraints.NotNull来注释相应的字段,如下面的例子所示:

public class Ebook extends Book implements Serializable {
    ...
    @NotNull
    private String format;
    ...
}

public class Paperback extends Book implements Serializable {
    ...
    @NotNull
    private String sizeIn;
    @NotNull
    private String weightLbs;
    ...
}

这一次,试图持久化这个ebook将导致类型javax.validation.ConstraintViolationException的异常,其中提到format不能是null

这仅仅解决了问题的一半。还可以通过本地查询插入带有null格式的行。阻止这种尝试意味着在数据库级别进行检查。

对于 MySQL,这可以通过为基类创建的一组触发器来实现(或者,在 PostgreSQL 和其他 RDBMSs 中,CHECK约束)。例如,以下触发器在数据库级别起作用,不允许null格式(在Ebook的情况下)和null大小或权重(在Paperback的情况下):

下面是EBook的触发器:

CREATE TRIGGER ebook_format_trigger
  BEFORE INSERT ON book
    FOR EACH ROW
      BEGIN
        IF NEW.DTYPE = 'Ebook' THEN
          IF NEW.format IS NULL THEN
            SIGNAL SQLSTATE '45000'
            SET MESSAGE_TEXT='The format of e-book cannot be null';
          END IF;
        END IF;
      END;

以下是Paperback的触发因素:

CREATE TRIGGER paperback_weight_trigger
BEFORE INSERT ON book
  FOR EACH ROW
    BEGIN
      IF NEW.DTYPE = 'Paperback' THEN
        IF NEW.weight_lbs IS NULL THEN
          SIGNAL SQLSTATE '45000'
          SET MESSAGE_TEXT='The weight of paperback cannot be null';
        END IF;
      END IF;
    END;

CREATE TRIGGER paperback_size_trigger
  BEFORE INSERT ON book
    FOR EACH ROW
      BEGIN
        IF NEW.DTYPE = 'Paperback' THEN
          IF NEW.size_in IS NULL THEN
            SIGNAL SQLSTATE '45000'
            SET MESSAGE_TEXT='The size of paperback cannot be null';
          END IF;
        END IF;
      END;

这些触发器应该添加到您的模式文件中。将它们放在 SQL 文件中需要您在application.properties中设置spring.datasource.separator:

spring.datasource.separator=^;

然后在 SQL 文件中,不在触发器内的所有;语句都需要用新的分隔符更新,如下例所示:

CREATE TRIGGER ebook_format_trigger
      ...
      END ^;

在本书捆绑的代码中,触发器被添加到了data-mysql.sql。最好将它们添加到schema-mysql.sql中,或者添加到 Flyway 或 Liquibase 的 SQL 文件中。我用这种方式让您看到 Hibernate 如何基于单个表继承注释生成 DDL 模式。

根据经验,数据库触发器对于实现复杂的数据完整性约束和规则非常有用。这里的 2 就是支持这种说法的一个例子。

优化鉴别器列的内存占用

调整列的大小和数据类型是优化数据库内存占用的重要步骤。鉴别器列是由 JPA 持久性提供者添加的,它的数据类型和大小是VARCHAR(31)。但是存储Paperback名至少需要 9 个字节,而存储Ebook名需要 4 个字节。想象一下,存储 100,000 本平装书和 500,000 本电子书。存储鉴别器列索引需要 100000÷9+500000÷4 = 2900000 字节,也就是 2.76MB。但是,将鉴别器列定义为TINYINT(1)怎么样呢?这一次,需要 1 个字节,所以计算变成 100000∫1+500000∫1 = 600000 字节,这是 0.57MB。这是更好的方式!

您可以通过@DiscriminatorColumn@DiscriminatorValue改变默认的鉴别器列。首先,使用@DiscriminatorColumn改变鉴别器列的类型和大小。第二,使用@DiscriminatorValue为每个类分配一个整数(这些整数应该进一步用于引用这些类):

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
    discriminatorType = DiscriminatorType.INTEGER,
    columnDefinition = "TINYINT(1)"
)

@DiscriminatorValue("1")
public class Book implements Serializable {
    ...
}

@Entity
@DiscriminatorValue("2")
public class Ebook extends Book implements Serializable {
    ...
}

@Entity
@DiscriminatorValue("3")
public class Paperback extends Book implements Serializable {
    ...
}

仅此而已!完整的应用可在 GitHub 3 上获得。

现在让我们来看看单表继承的一些优缺点。

优点:

  • 子类列不允许约束,但是,如你所见,这个问题有解决方案。

  • 读写速度很快

  • @ManyToOne@OneToOne@OneToMany是高效的

  • 基类属性可以是不可空的

    缺点:

第 139 项:如何从一个 SINGLE_TABLE 继承层次结构中获取某些子类

该项目使用来自项目 138 的领域模型和知识;因此,考虑先熟悉那个项目。

所以,在AuthorBook之间,有一个双向的懒惰@OneToMany联想。EbookPaperback实体依靠SINGLE_TABLE继承策略来扩展Book实体。

book表包含与Book实体以及EbookPaperback实体相关联的列。它还包含一个名为dtype的栏目。这就是所谓的鉴别器列。

您可以通过其专用的存储库获取某个子类(例如,Ebook),如下例所示(这里,查询通过标题获取一个Ebook):

@Repository
@Transactional(readOnly = true)
public interface EbookRepository extends JpaRepository<Ebook, Long> {

    Ebook findByTitle(String title);
}

SELECT
  ebook0_.id AS id2_1_,
  ebook0_.author_id AS author_i8_1_,
  ebook0_.isbn AS isbn3_1_,
  ebook0_.title AS title4_1_,
  ebook0_.size_in AS size_in6_1_,
  ebook0_.weight_lbs AS weight_l7_1_
FROM book ebook0_
WHERE ebook0_.dtype = 'Ebook'
AND ebook0_.title = ?

注意WHERE子句。Hibernate 增加了一个基于dtype的条件,只获取电子书。

这绝对很棒,但并不总是这样。例如,考虑EbookRepository中的以下@Query:

@Repository
@Transactional(readOnly = true)
public interface EbookRepository extends JpaRepository<Ebook, Long> {

    @Query("SELECT b FROM Author a JOIN a.books b WHERE a.name = ?1)
    public Ebook findByAuthorName(String name);
}

这一次,触发的SELECT看起来如下:

SELECT
  books1_.id AS id2_1_,
  books1_.author_id AS author_i8_1_,
  books1_.isbn AS isbn3_1_,
  books1_.title AS title4_1_,
  books1_.format AS format5_1_,
  books1_.size_in AS size_in6_1_,
  books1_.weight_lbs AS weight_l7_1_,
  books1_.dtype AS dtype1_1_
FROM author author0_
INNER JOIN book books1_
  ON author0_.id = books1_.author_id
WHERE author0_.name = ?

鉴别器列(dtype)没有自动添加到WHERE子句中,所以这个查询不会只获取Ebook。显然,这是不行的!这个问题的解决方案依赖于一个显式的TYPE表达式,如下所示(参见粗体查询部分):

@Repository
@Transactional(readOnly = true)
public interface EbookRepository extends JpaRepository<Ebook, Long> {

    @Query("SELECT b FROM Author a JOIN a.books b
            WHERE a.name = ?1 AND TYPE(b) = 'Ebook'")
    public Ebook findByAuthorName(String name);
}

这次触发的SELECT如下:

SELECT
  books1_.id AS id2_1_,
  books1_.author_id AS author_i8_1_,
  books1_.isbn AS isbn3_1_,
  books1_.title AS title4_1_,
  books1_.format AS format5_1_,
  books1_.size_in AS size_in6_1_,
  books1_.weight_lbs AS weight_l7_1_,
  books1_.dtype AS dtype1_1_
FROM author author0_
INNER JOIN book books1_
  ON author0_.id = books1_.author_id
WHERE author0_.name = ?
AND books1_.dtype = 'Ebook'

感谢TYPE表情,事情又回到正轨了!取一个AuthorEbook类型的Book怎么样?通过TYPE表达式,这种查询可以在BookRepository中编写如下(查询定义与前一个完全相同,但它被放在BookRepository中,并返回一个类型为EbookBook):

@Repository
@Transactional(readOnly = true)
public interface BookRepository extends JpaRepository<Book, Long> {

    @Query("SELECT b FROM Author a JOIN a.books b
            WHERE a.name = ?1 AND TYPE(b) = 'Ebook'")
    public Book findByAuthorName(String name);
}

完整的应用可在 GitHub 4 上获得。

第 140 项:如何有效地使用连接表继承

连接表是另一种 JPA 继承策略。按照这种策略,继承层次结构中的所有类都通过数据库中的单个表来表示。考虑图 15-3 中给出的继承层次。

img/487471_1_En_15_Fig3_HTML.jpg

图 15-3

连接表继承域模型

AuthorBook之间,有一个双向懒惰的@OneToMany关联。Author实体可以被视为根类,因为没有作者就没有书。Book实体是基类。为了使用连接表继承策略,这个类用@Inheritance(strategy = InheritanceType.JOINED)进行了注释。EbookPaperback实体扩展了Book实体;所以,他们不需要自己的@Id。形成这种继承策略的表格如图 15-4 所示。

img/487471_1_En_15_Fig4_HTML.jpg

图 15-4

连接表继承策略的表

这里列出了Book基类和子类的相关代码:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Book implements Serializable {
    ...
}

@Entity
@PrimaryKeyJoinColumn(name="ebook_book_id")
public class Ebook extends Book implements Serializable {
    ...
}

@Entity
@PrimaryKeyJoinColumn(name="paperback_book_id")
public class Paperback extends Book implements Serializable {
    ...
}

默认情况下,子类表包含一个主键列,该列也充当外键。这个外键引用了基类表的主键。您可以通过用@PrimaryKeyJoinColumn注释子类来定制这个外键。例如,EbookPaperback子类依赖这个注释来定制外键列的名称:

@Entity
@PrimaryKeyJoinColumn(name="ebook_book_id")
public class Ebook extends Book implements Serializable {
    ...
}

@Entity
@PrimaryKeyJoinColumn(name="paperback_book_id")
public class Paperback extends Book implements Serializable {
    ...
}

默认情况下,基类的主键列名和子类的主键列名是相同的。

持久数据

下面的服务方法持久化一个包含三本书的Author,这三本书是通过BookEbookPaperback实体创建的:

public void persistAuthorWithBooks() {

    Author author = new Author();
    author.setName("Alicia Tom");
    author.setAge(38);
    author.setGenre("Anthology");

    Book book = new Book();
    book.setIsbn("001-AT");
    book.setTitle("The book of swords");

    Paperback paperback = new Paperback();
    paperback.setIsbn("002-AT");
    paperback.setTitle("The beatles anthology");
    paperback.setSizeIn("7.5 x 1.3 x 9.2");
    paperback.setWeightLbs("2.7");

    Ebook ebook = new Ebook();
    ebook.setIsbn("003-AT");
    ebook.setTitle("Anthology myths");
    ebook.setFormat("kindle");

    author.addBook(book); // use addBook() helper
    author.addBook(paperback);
    author.addBook(ebook);

    authorRepository.save(author);
}

保存author实例会触发以下 SQL 语句:

INSERT INTO author (age, genre, name)
  VALUES (?, ?, ?)
Binding:[38, Anthology, Alicia Tom]

INSERT INTO book (author_id, isbn, title)
  VALUES (?, ?, ?)
Binding:[1, 001-AT, The book of swords]

INSERT INTO book (author_id, isbn, title)
  VALUES (?, ?, ?)
Binding:[1, 002-AT, The beatles anthology]
INSERT INTO paperback (size_in, weight_lbs, paperback_book_id)
  VALUES (?, ?, ?)
Binding:[ 7.5 x 1.3 x 9.2, 2.7, 2]

INSERT INTO book (author_id, isbn, title)
  VALUES (?, ?, ?)
Binding:[1, 003-AT, Anthology myths]
INSERT INTO ebook (format, ebook_book_id)
  VALUES (?, ?)
Binding:[kindle, 3]

这一次,需要比单表继承策略更多的INSERT语句(参见第 138 项)。主要是,基类的数据被插入到book表中,而Ebook类,分别是Paperback类的数据被放入到ebookpaperback表中。插入越多,性能损失的机会就越大。

查询和连接表继承

现在,让我们来看看获取数据的效率。考虑以下BookRepository:

@Repository
@Transactional(readOnly = true)
public interface BookRepository extends JpaRepository<Book, Long> {

    @Query("SELECT b FROM Book b WHERE b.author.id = ?1")
    List<Book> fetchBooksByAuthorId(Long authorId);

    Book findByTitle(String title);
}

通过作者标识符获取图书

让我们打电话给fetchBooksByAuthorId():

List<Book> books = bookRepository.fetchBooksByAuthorId(1L);

触发的SELECT如下:

SELECT
  book0_.id AS id1_1_,
  book0_.author_id AS author_i4_1_,
  book0_.isbn AS isbn2_1_,
  book0_.title AS title3_1_,
  book0_1_.format AS format1_2_,
  book0_2_.size_in AS size_in1_3_,
  book0_2_.weight_lbs AS weight_l2_3_,
  CASE
    WHEN book0_1_.ebook_book_id IS NOT NULL THEN 1
    WHEN book0_2_.paperback_book_id IS NOT NULL THEN 2
    WHEN book0_.id IS NOT NULL THEN 0
  END AS clazz_
FROM book book0_
LEFT OUTER JOIN ebook book0_1_
  ON book0_.id = book0_1_.ebook_book_id
LEFT OUTER JOIN paperback book0_2_
  ON book0_.id = book0_2_.paperback_book_id
WHERE book0_.author_id = ?

有一个单独的SELECT,但是 Hibernate 必须连接每个子类表。因此,子类表的数量决定了多态查询中连接的数量(对于 n 子类,将有 n 连接)。此外,连接的数量会影响查询速度和执行计划的效率。

按书名取书

让我们为每本书调用findByTitle():

Book b1 = bookRepository.findByTitle("The book of swords");    // Book
Book b2 = bookRepository.findByTitle("The beatles anthology"); // Paperback
Book b3 = bookRepository.findByTitle("Anthology myths");       // Ebook

触发的SELECT对于所有三种类型的图书都是相同的:

SELECT
  book0_.id AS id1_1_,
  book0_.author_id AS author_i4_1_,
  book0_.isbn AS isbn2_1_,
  book0_.title AS title3_1_,
  book0_1_.format AS format1_2_,
  book0_2_.size_in AS size_in1_3_,
  book0_2_.weight_lbs AS weight_l2_3_,
  CASE
    WHEN book0_1_.ebook_book_id IS NOT NULL THEN 1
    WHEN book0_2_.paperback_book_id IS NOT NULL THEN 2
    WHEN book0_.id IS NOT NULL THEN 0
  END AS clazz_
FROM book book0_
LEFT OUTER JOIN ebook book0_1_
  ON book0_.id = book0_1_.ebook_book_id
LEFT OUTER JOIN paperback book0_2_
  ON book0_.id = book0_2_.paperback_book_id
WHERE book0_.title = ?

同样,只有一个SELECT,但是 Hibernate 必须连接每个子类表。因此,通过基类存储库获取子类是没有效率的。让我们看看子类的专用存储库会发生什么。

去拿平装书

考虑下面列出的Paperback存储库:

@Repository
@Transactional(readOnly = true)
public interface PaperbackRepository    extends JpaRepository<Paperback, Long> {

    Paperback findByTitle(String title);
}

现在,让我们触发两个查询。第一个查询使用标识一个Book的标题。第二个查询使用标识一个Paperback的标题:

// this is a Book
Paperback p1 = paperbackRepository.findByTitle("The book of swords");

// this is a Paperback
Paperback p2 = paperbackRepository.findByTitle("The beatles anthology");

两个查询触发相同的SELECT ( p1将是null,而p2将获取一个Paperback):

SELECT
  paperback0_.paperback_book_id AS id1_1_,
  paperback0_1_.author_id AS author_i4_1_,
  paperback0_1_.isbn AS isbn2_1_,
  paperback0_1_.title AS title3_1_,
  paperback0_.size_in AS size_in1_3_,
  paperback0_.weight_lbs AS weight_l2_3_
FROM paperback paperback0_
INNER JOIN book paperback0_1_
  ON paperback0_.paperback_book_id = paperback0_1_.id
WHERE paperback0_1_.title = ?

通过专用存储库获取子类需要与基类表进行一次连接。

如果可能的话,避免通过基类存储库获取子类。使用子类的专用库。在第一种情况下,子类的数量影响连接的数量,而在第二种情况下,子类和基类表之间只有一个连接。换句话说,直接使用查询而不是子类实体。

当然,这不像依赖子类的专用库那样实际。注意,我们在BookRepository中定义了findByTitle()。如果我们想从EbookRepositoryPaperbackRepository中使用它,那么复制它是不实际的(一般来说,在所有存储库中复制查询方法是不实际的)。在这种情况下,首先在@NoRepositoryBean类中定义findByTitle():

@NoRepositoryBean
public interface BookBaseRepository<T extends Book>
                            extends JpaRepository<T, Long> {
    T findByTitle(String title);
    @Query(value="SELECT b FROM #{#entityName} AS b WHERE b.isbn = ?1")

    T fetchByIsbn(String isbn);
}

接下来,BookRepositoryEbookRepositoryPaperbackRepository延伸BookBaseRepository。通过这种方式,findByTitle()findByIsbn()可以在所有扩展基本存储库的存储库中使用。完整的应用可在 GitHub 5 上获得。

获取作者和相关书籍

考虑下面的Author存储库:

@Repository
@Transactional(readOnly = true)
public interface AuthorRepository extends JpaRepository<Author, Long> {

    Author findByName(String name);

    @Query("SELECT a FROM Author a JOIN FETCH a.books b")
    public Author findAuthor();
}

调用findByName()将获取没有相关书籍的作者:

@Transactional(readOnly = true)
public void fetchAuthorAndBooksLazy() {
    Author author = authorRepository.findByName("Alicia Tom");
    List<Book> books = author.getBooks();
}

调用getBooks()触发第二个查询:

-- fetch the author
SELECT
  author0_.id AS id1_0_,
  author0_.age AS age2_0_,
  author0_.genre AS genre3_0_,
  author0_.name AS name4_0_
FROM author author0_
WHERE author0_.name = ?

-- fetch the books via getBooks()
SELECT
  books0_.author_id AS author_i4_1_0_,
  books0_.id AS id1_1_0_,
  books0_.id AS id1_1_1_,
  books0_.author_id AS author_i4_1_1_,
  books0_.isbn AS isbn2_1_1_,
  books0_.title AS title3_1_1_,
  books0_1_.format AS format1_2_1_,
  books0_2_.size_in AS size_in1_3_1_,
  books0_2_.weight_lbs AS weight_l2_3_1_,
  CASE
    WHEN books0_1_.ebook_book_id IS NOT NULL THEN 1
    WHEN books0_2_.paperback_book_id IS NOT NULL THEN 2
    WHEN books0_.id IS NOT NULL THEN 0
  END AS clazz_1_
FROM book books0_
LEFT OUTER JOIN ebook books0_1_
  ON books0_.id = books0_1_.ebook_book_id
LEFT OUTER JOIN paperback books0_2_
  ON books0_.id = books0_2_.paperback_book_id
WHERE books0_.author_id = ?

第二个SELECT也有同样的缺点。每个子类表都有一个连接。因此,组合多态查询和深层类层次结构和/或大量子类会导致性能下降。

另一方面,由于有了JOIN FETCH,调用findAuthor()将在同一个SELECT中获取作者和相关书籍:

@Transactional(readOnly = true)
public void fetchAuthorAndBooksEager() {
    Author author = authorRepository.findAuthor();
}

这里列出了触发的SELECT:

SELECT
  author0_.id AS id1_0_0_,
  books1_.id AS id1_1_1_,
  author0_.age AS age2_0_0_,
  author0_.genre AS genre3_0_0_,
  author0_.name AS name4_0_0_,
  books1_.author_id AS author_i4_1_1_,
  books1_.isbn AS isbn2_1_1_,
  books1_.title AS title3_1_1_,
  books1_1_.format AS format1_2_1_,
  books1_2_.size_in AS size_in1_3_1_,
  books1_2_.weight_lbs AS weight_l2_3_1_,
  CASE
    WHEN books1_1_.ebook_book_id IS NOT NULL THEN 1
    WHEN books1_2_.paperback_book_id IS NOT NULL THEN 2
    WHEN books1_.id IS NOT NULL THEN 0
  END AS clazz_1_,
  books1_.author_id AS author_i4_1_0__, books1_.id AS id1_1_0__
FROM author author0_
INNER JOIN book books1_
  ON author0_.id = books1_.author_id
LEFT OUTER JOIN ebook books1_1_
  ON books1_.id = books1_1_.ebook_book_id
LEFT OUTER JOIN paperback books1_2_
  ON books1_.id = books1_2_.paperback_book_id

这一次,JPA 持久性提供者需要三个连接。因此,对于 n 子类,将有 n +1 个连接。这样效率不高。

下面是连接表继承的一些优点和缺点。

优点:

  • 持久化子类实体需要两个INSERT语句

  • 只有通过子类的专用存储库,读取才是有效的(换句话说,直接对子类实体使用查询)

  • 数据库必须索引基类和所有子类主键

  • 在多态查询的情况下,对于 n 子类,Hibernate 需要 n 或者 n +1 个连接。这可能会导致查询速度变慢,并增加确定最有效执行计划的难度

  • 基类和子类属性可以是不可空的

  • 只要不需要多态查询,这种策略就适合于深度类层次结构和/或大量子类

    缺点:

完整的应用可在 GitHub 6 上获得。

如何使用 JPA 加入继承策略和策略设计模式

首先,努力使用继承策略,如SINGLE_TABLEJOINEDTABLE_PER_CLASS结合一个软件设计模式(如模板、状态、策略、访问者等)。).将继承策略与软件设计模式结合起来是利用 JPA 继承的最佳方式。要将特定属性从一个基类传播到所有子类,可以使用@MappedSuperclass

策略模式是一种众所周知的行为模式。简而言之,策略模式允许您定义一组算法,将每个算法包装在一个类中,并使它们可以互换。

例如,让我们假设在每天结束时,你的书店交付当天订购的书籍。对于电子书,你通过电子邮件发送下载链接,而对于平装书,我们发送包裹。当然,也可以采用其他策略,但还是让事情简单化吧。

您可以通过编写以下接口来开始开发:

public interface Delivery<T extends Book> {

    Class<? extends Book> ofBook();
    void deliver(T book);
}

deliver()方法是动作发生的地方,而ofBook()方法只是返回利用策略实现的图书的类类型。你会立刻明白为什么你需要这个方法。现在,让我们添加策略(为了简单起见,我们通过System.out.println()来模拟交付):

@Component
public class PaperbackDeliver implements Delivery<Paperback> {

    @Override
    public void deliver(Paperback book) {
        System.out.println("We've sent you a parcel containing the title "
            + book.getTitle() + " with a size of '" + book.getSizeIn()
            + "' and a weight of " + book.getWeightLbs());
    }

    @Override
    public Class<? extends Book> ofBook() {
        return Paperback.class;
    }
}

@Component
public class EbookDeliver implements Delivery<Ebook> {

    @Override
    public void deliver(Ebook book) {
        System.out.println("You can download the book named '"
            + book.getTitle() + "' from the following link: http://bookstore/" + book.getFormat() + "/" + book.getTitle());
    }

    @Override
    public Class<? extends Book> ofBook() {
        return Ebook.class;
    }
}

接下来,您需要一个使用策略的服务。策略豆(EbookDeliverPaperbackDeliver)作为List<Delivery>被 Spring 自动注入。所以,新的策略会自动注入到DeliverService中。此外,你循环这个列表并使用ofBook()来构建一个策略图。该图中的是 book 类类型(例如Ebook.classPaperback.class),而是策略本身(策略 bean 实例)。这样,您可以根据图书类型(ebookpaperback)调用适当的deliver()方法:

public interface Deliverable {

    void process();
}

@Service
public class DeliverService implements Deliverable {

    private final BookRepository bookRepository;
    private final List<Delivery> deliverStrategies;

    private final Map<Class<? extends Book>, Delivery>
        deliverStrategiesMap = new HashMap<>();

    public DeliverService(BookRepository bookRepository,
                            List<Delivery> deliverStrategies) {
        this.bookRepository = bookRepository;
        this.deliverStrategies = deliverStrategies;
    }

    @PostConstruct
    public void init() {
        deliverStrategies.forEach((deliverStrategy) -> {
            deliverStrategiesMap.put(deliverStrategy.ofBook(),
                                                    deliverStrategy);
        });
    }

    @Override
    public void process() {

        // we just need some books to deliver
        List<Book> allBooks = bookRepository.findAll();

        for (Book book : allBooks) {
            Delivery deliveryStrategy
                = deliverStrategiesMap.get(book.getClass());
            deliveryStrategy.deliver(book);
        }
    }
}

process()方法负责应用策略。你循环应该交付的书籍,应用相应的策略。只是为了获取一些书籍进行测试,您可以应用一个findAll()查询。

完整的应用可在 GitHub 7 上获得。而且,在 GitHub8上,你可以找到另一个使用访问者设计模式的例子。

第 141 项:如何有效地使用每个类的表继承

每类一张表是另一种 JPA 继承策略。按照这种策略,继承层次结构中的所有类都通过数据库中的单个表来表示。每个子类表存储从超类表继承的列(基类)。考虑图 15-5 中给出的继承层次。

img/487471_1_En_15_Fig5_HTML.jpg

图 15-5

每类表继承域模型

AuthorBook之间,有一个双向懒惰的@OneToMany关联。Author实体可以被看作是根类,因为没有作者就没有书。Book实体是基类。为了采用每类一个表的继承策略,这个类用@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)进行了注释。EbookPaperback实体扩展了Book实体,所以不需要自己的@Id。形成这种继承策略的表格如图 15-6 所示。

img/487471_1_En_15_Fig6_HTML.jpg

图 15-6

每类表继承策略的表

每个子类表包含一个主键列。为了确保子类表中主键的唯一性,每类表策略不能依赖于IDENTITY生成器。尝试使用IDENTITY生成器会导致类型为Cannot use identity column key generation with <union-subclass> mapping的异常。

这是 MySQL 等 RDBMSs 的一个重要缺点。不允许使用IDENTITY,也不支持SEQUENCE(MySQL 不支持数据库序列;因此不支持SEQUENCE策略)。TABLE生成器类型的伸缩性不好,比IDENTITYSEQUENCE生成器类型慢得多,即使对于单个数据库连接也是如此。所以,你应该避免 MySQL 和每类一个表的继承策略的结合。

这里列出了Book基类和子类的相关代码:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Book implements Serializable {
    ...
}

@Entity
public class Ebook extends Book implements Serializable {
    ...
}

@Entity
public class Paperback extends Book implements Serializable {
    ...
}

持久数据

下面的服务方法持久化一个包含三本书的Author,这三本书是通过BookEbookPaperback实体创建的:

public void persistAuthorWithBooks() {

    Author author = new Author();
    author.setName("Alicia Tom");
    author.setAge(38);
    author.setGenre("Anthology");

    Book book = new Book();
    book.setIsbn("001-AT");
    book.setTitle("The book of swords");

    Paperback paperback = new Paperback();
    paperback.setIsbn("002-AT");
    paperback.setTitle("The beatles anthology");
    paperback.setSizeIn("7.5 x 1.3 x 9.2");
    paperback.setWeightLbs("2.7");

    Ebook ebook = new Ebook();
    ebook.setIsbn("003-AT");
    ebook.setTitle("Anthology myths");
    ebook.setFormat("kindle");

    author.addBook(book); // use addBook() helper
    author.addBook(paperback);
    author.addBook(ebook);

    authorRepository.save(author);
}

保存author实例会触发以下 SQL 语句:

INSERT INTO author (age, genre, name)
  VALUES (?, ?, ?)
Binding:[38, Anthology, Alicia Tom]

INSERT INTO book (author_id, isbn, title, id)
  VALUES (?, ?, ?, ?)
Binding:[1, 001-AT, The book of swords, 1]

INSERT INTO paperback (author_id, isbn, title, size_in, weight_lbs, id)
  VALUES (?, ?, ?, ?, ?, ?)
Binding:[1, 002-AT, The beatles anthology, 7.5 x 1.3 x 9.2, 2.7, 2]

INSERT INTO ebook (author_id, isbn, title, format, id)
  VALUES (?, ?, ?, ?, ?)
Binding:[1, 003-AT, Anthology myths, kindle, 3]

每个类的表为每个子类触发一个INSERT,因此它比连接表继承策略更有效。

查询和每表类继承

现在,让我们来看看获取数据的效率。考虑以下BookRepository:

@Repository
@Transactional(readOnly = true)
public interface BookRepository extends JpaRepository<Book, Long> {

    @Query("SELECT b FROM Book b WHERE b.author.id = ?1")
    List<Book> fetchBooksByAuthorId(Long authorId);

    Book findByTitle(String title);
}

通过作者标识符获取图书

让我们打电话给fetchBooksByAuthorId():

List<Book> books = bookRepository.fetchBooksByAuthorId(1L);

触发的SELECT如下:

SELECT
  book0_.id AS id1_1_,
  book0_.author_id AS author_i4_1_,
  book0_.isbn AS isbn2_1_,
  book0_.title AS title3_1_,
  book0_.format AS format1_2_,
  book0_.size_in AS size_in1_3_,
  book0_.weight_lbs AS weight_l2_3_,
  book0_.clazz_ AS clazz_
FROM (SELECT
  id, isbn, title, author_id,
  NULL AS format, NULL AS size_in, NULL AS weight_lbs, 0 AS clazz_
FROM book
UNION
SELECT
  id, isbn, title, author_id, format,
  NULL AS size_in, NULL AS weight_lbs, 1 AS clazz_
FROM ebook
UNION
SELECT
  id, isbn, title, author_id,
  NULL AS format, size_in, weight_lbs, 2 AS clazz_
FROM paperback) book0_
WHERE book0_.author_id = ?

在多态查询的情况下,Hibernate 依靠 SQL 联合从基类和每个子类表中获取数据。显然,由于需要更多的联合,多态查询的效率会降低。

按书名取书

让我们为每本书调用findByTitle():

Book b1 = bookRepository.findByTitle("The book of swords");    // Book
Book b2 = bookRepository.findByTitle("The beatles anthology"); // Paperback
Book b3 = bookRepository.findByTitle("Anthology myths");       // Ebook

触发的SELECT对于所有三种类型的图书都是相同的:

SELECT
  book0_.id AS id1_1_,
  book0_.author_id AS author_i4_1_,
  book0_.isbn AS isbn2_1_,
  book0_.title AS title3_1_,
  book0_.format AS format1_2_,
  book0_.size_in AS size_in1_3_,
  book0_.weight_lbs AS weight_l2_3_,
  book0_.clazz_ AS clazz_
FROM (SELECT
  id, isbn, title, author_id,
  NULL AS format, NULL AS size_in, NULL AS weight_lbs, 0 AS clazz_
FROM book
UNION
SELECT
  id, isbn, title, author_id, format,
  NULL AS size_in, NULL AS weight_lbs, 1 AS clazz_
FROM ebook
UNION
SELECT
  id, isbn, title, author_id,
  NULL AS format, size_in, weight_lbs, 2 AS clazz_
FROM paperback) book0_
WHERE book0_.title = ?

同样,Hibernate 依靠 SQL 联合从基类和每个子类表中获取数据。因此,通过基类存储库获取子类实体是没有效率的,应该避免。

去拿平装书

考虑下面列出的Paperback存储库:

@Repository
@Transactional(readOnly = true)
public interface PaperbackRepository     extends JpaRepository<Paperback, Long> {

    Paperback findByTitle(String title);
}

现在,让我们触发两个查询。第一个查询使用标识一个Book的标题。第二个查询使用标识一个Paperback的标题:

// this is a Book
Paperback p1 = paperbackRepository.findByTitle("The book of swords");

// this is a Paperback
Paperback p2 = paperbackRepository.findByTitle("The beatles anthology");

两个查询触发相同的SELECT ( p1将是null,而p2将获取一个Paperback):

SELECT
  paperback0_.id AS id1_1_,
  paperback0_.author_id AS author_i4_1_,
  paperback0_.isbn AS isbn2_1_,
  paperback0_.title AS title3_1_,
  paperback0_.size_in AS size_in1_3_,
  paperback0_.weight_lbs AS weight_l2_3_
FROM paperback paperback0_
WHERE paperback0_.title = ?

通过专用库获取子类是高效的。

如果可能的话,避免通过基类存储库获取子类。最好使用子类专用的存储库。在第一种情况下,子类的数量影响联合的数量,而在第二种情况下,将不存在联合。换句话说,最好直接对子类实体使用查询。

当然,这不像依赖子类的专用库那样实际。注意,我们在BookRepository中定义了findByTitle()。如果我们想从EbookRepositoryPaperbackRepository中使用它,那么复制它是不实际的(一般来说,在所有存储库中复制查询方法是不实际的)。在这种情况下,首先在@NoRepositoryBean类中定义findByTitle():

@NoRepositoryBean
public interface BookBaseRepository<T extends Book>
                            extends JpaRepository<T, Long> {
    T findByTitle(String title);
    @Query(value="SELECT b FROM #{#entityName} AS b WHERE b.isbn = ?1")

    T fetchByIsbn(String isbn);
}

接下来,BookRepositoryEbookRepositoryPaperbackRepository延伸BookBaseRepository。通过这种方式,findByTitle()findByIsbn()可以在所有扩展基本存储库的存储库中使用。完整的应用可在 GitHub 9 上获得。

获取作者和相关书籍

考虑下面的Author存储库:

@Repository
@Transactional(readOnly = true)
public interface AuthorRepository extends JpaRepository<Author, Long> {

    Author findByName(String name);

    @Query("SELECT a FROM Author a JOIN FETCH a.books b")
    public Author findAuthor();
}

调用findByName()将获取没有相关书籍的作者:

@Transactional(readOnly = true)
public void fetchAuthorAndBooksLazy() {
    Author author = authorRepository.findByName("Alicia Tom");
    List<Book> books = author.getBooks();
}

调用getBooks()触发第二个查询:

-- fetch the author
SELECT
  author0_.id AS id1_0_,
  author0_.age AS age2_0_,
  author0_.genre AS genre3_0_,
  author0_.name AS name4_0_
FROM author author0_
WHERE author0_.name = ?

-- fetch the books via getBooks()
SELECT
  books0_.author_id AS author_i4_1_0_,
  books0_.id AS id1_1_0_,
  books0_.id AS id1_1_1_,
  books0_.author_id AS author_i4_1_1_,
  books0_.isbn AS isbn2_1_1_,
  books0_.title AS title3_1_1_,
  books0_.format AS format1_2_1_,
  books0_.size_in AS size_in1_3_1_,
  books0_.weight_lbs AS weight_l2_3_1_,
  books0_.clazz_ AS clazz_1_
FROM (SELECT
  id, isbn, title, author_id,
  NULL AS format, NULL AS size_in, NULL AS weight_lbs, 0 AS clazz_
FROM book
UNION
SELECT
  id, isbn, title, author_id, format,
  NULL AS size_in, NULL AS weight_lbs, 1 AS clazz_
FROM ebook
UNION
SELECT
  id, isbn, title, author_id,
  NULL AS format, size_in, weight_lbs, 2 AS clazz_
FROM paperback) books0_
WHERE books0_.author_id = ?

第二个SELECT也有同样的缺点。每个子类表都有一个联合。所以,深层次的类和/或大量的子类会导致性能下降。

另一方面,由于有了JOIN FETCH,调用findAuthor()将在同一个SELECT中获取作者和相关书籍:

@Transactional(readOnly = true)
public void fetchAuthorAndBooksEager() {
    Author author = authorRepository.findAuthor();
}

不幸的是,触发的SELECT并不高效,因为它需要每个子类一个联合( n 子类导致 n 联合):

SELECT
  author0_.id AS id1_0_0_,
  books1_.id AS id1_1_1_,
  author0_.age AS age2_0_0_,
  author0_.genre AS genre3_0_0_,
  author0_.name AS name4_0_0_,
  books1_.author_id AS author_i4_1_1_,
  books1_.isbn AS isbn2_1_1_,
  books1_.title AS title3_1_1_,
  books1_.format AS format1_2_1_,
  books1_.size_in AS size_in1_3_1_,
  books1_.weight_lbs AS weight_l2_3_1_,
  books1_.clazz_ AS clazz_1_,
  books1_.author_id AS author_i4_1_0__,
  books1_.id AS id1_1_0__
FROM author author0_
INNER JOIN (SELECT
  id, isbn, title, author_id,
  NULL AS format, NULL AS size_in, NULL AS weight_lbs, 0 AS clazz_
FROM book
UNION
SELECT
  id, isbn, title, author_id, format,
  NULL AS size_in, NULL AS weight_lbs, 1 AS clazz_
FROM ebook
UNION
SELECT
  id, isbn, title, author_id,
  NULL AS format, size_in, weight_lbs, 2 AS clazz_
FROM paperback) books1_
  ON author0_.id = books1_.author_id

现在,让我们考虑一下连接表继承的优缺点。

优点:

  • IDENTITY发生器不能使用

  • 只有通过子类的专用存储库,读取才是有效的(换句话说,最好直接对子类实体使用查询)

  • 在多态查询的情况下,对于 n 子类,Hibernate 需要 n 联合,这可能会导致严重的性能损失

  • 写入速度很快,因为每个子类都有一个INSERT

  • 基类和子类属性可以是不可空的

    缺点:

完整的应用可在 GitHub 10 上获得。

第 142 项:如何有效地使用@MappedSuperclass

你已经在第 24 项第 87 项中看到@MappedSuperclass在工作。

@MappedSuperclass是一个实体级的注释,它有助于形成一个继承模型,类似于每类一个表的策略,但是有一个不是实体的基类。它没有在数据库表中具体化。基类用@MappedSuperclass标注,可以是abstract也可以不是。它的子类将继承它的属性,并将它们存储在自己属性旁边的子类表中。考虑图 15-7 中给出的继承层次。

img/487471_1_En_15_Fig7_HTML.jpg

图 15-7

映射超类领域模型

AuthorBook之间有一个单向的懒惰@ManyToOne联想。由于Book不是一个实体,它不支持关联;因此,Author实体不能定义一个@OneToMany关系。这个Author实体可以被看作是根类,因为没有作者就没有书。Book实体是非实体基类EbookPaperback实体扩展了Book实体,所以它们不需要自己的@Id。表格关系如图 15-8 所示(注意没有book表格)。

img/487471_1_En_15_Fig8_HTML.jpg

图 15-8

映射超类表

这里列出了Book基类的相关代码:

@MappedSuperclass
public abstract class Book implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String isbn;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private Author author;

    // getters and setters omitted for brevity
}

@Entity
public class Ebook extends Book implements Serializable {
    ...
}

@Entity
public class Paperback extends Book implements Serializable {
    ...
}

持久数据

下面的服务方法持久化一个包含三本书的Author,这三本书是通过BookEbookPaperback实体创建的:

public void persistAuthorWithBooks() {

    Author author = new Author();
    author.setName("Alicia Tom");
    author.setAge(38);
    author.setGenre("Anthology");

    Paperback paperback = new Paperback();
    paperback.setIsbn("002-AT");
    paperback.setTitle("The beatles anthology");
    paperback.setSizeIn("7.5 x 1.3 x 9.2");
    paperback.setWeightLbs("2.7");
    paperback.setAuthor(author);

    Ebook ebook = new Ebook();
    ebook.setIsbn("003-AT");
    ebook.setTitle("Anthology myths");
    ebook.setFormat("kindle");
    ebook.setAuthor(author);

    authorRepository.save(author);
    paperbackRepository.save(paperback);
    ebookRepository.save(ebook);
}

保存authorpaperbackebook实例会触发以下 SQL 语句:

INSERT INTO author (age, genre, name)
  VALUES (?, ?, ?)
Binding:[38, Anthology, Alicia Tom]

INSERT INTO paperback (author_id, isbn, title, size_in, weight_lbs)
  VALUES (?, ?, ?, ?, ?)
Binding:[1, 002-AT, The beatles anthology, 7.5 x 1.3 x 9.2, 2.7]

INSERT INTO ebook (author_id, isbn, title, format)
  VALUES (?, ?, ?, ?)
Binding:[1, 003-AT, Anthology myths, kindle]

写入是高效的。每个实体实例有一个INSERT

去拿平装书

考虑下面列出的Paperback存储库:

@Repository
@Transactional(readOnly = true)
public interface PaperbackRepository     extends JpaRepository<Paperback, Long> {

    Paperback findByTitle(String title);

    @Query("SELECT e FROM Paperback e JOIN FETCH e.author")
    Paperback fetchByAuthorId(Long id);
}

现在,让我们通过findByTitle()触发两个查询。第一个查询使用标识一个Ebook的标题。第二个查询使用标识一个Paperback的标题:

// this is a Ebook
Paperback p1 = paperbackRepository.findByTitle("Anthology myths");

// this is a Paperback
Paperback p2 = paperbackRepository.findByTitle("The beatles anthology");

两个查询触发相同的SELECT ( p1将是null,而p2将获取一个Paperback):

SELECT
  paperback0_.id AS id1_2_,
  paperback0_.author_id AS author_i6_2_,
  paperback0_.isbn AS isbn2_2_,
  paperback0_.title AS title3_2_,
  paperback0_.size_in AS size_in4_2_,
  paperback0_.weight_lbs AS weight_l5_2_
FROM paperback paperback0_
WHERE paperback0_.title = ?

这个查询非常简单高效。

把平装本的作者找来怎么样?这可以通过调用fetchByAuthorId()来完成。因为这种查询方法依赖于JOIN FETCH,所以作者与平装本在同一个SELECT中被取出,如下所示:

SELECT
  paperback0_.id AS id1_2_0_,
  author1_.id AS id1_0_1_,
  paperback0_.author_id AS author_i6_2_0_,
  paperback0_.isbn AS isbn2_2_0_,
  paperback0_.title AS title3_2_0_,
  paperback0_.size_in AS size_in4_2_0_,
  paperback0_.weight_lbs AS weight_l5_2_0_,
  author1_.age AS age2_0_1_,
  author1_.genre AS genre3_0_1_,
  author1_.name AS name4_0_1_
FROM paperback paperback0_
INNER JOIN author author1_
  ON paperback0_.author_id = author1_.id

该查询使用单个JOIN并且是高效的。因为Author没有关联,所以没有getBooks()getEbooks()getPaperbacks()

现在,我们来考虑一下@MappedSuperclass的一些利弊。

优点:

  • 无法查询基类

  • 不允许多态查询和关联

    根据经验,@MappedSuperclass非常适合将特定属性从基类传播到所有子类,因为对象层次结构的可见性保持在对象域级别。不要使用SINGLE_TABLEJOINEDTABLE_PER_CLASS这样的继承策略来完成这样的任务。依靠SINGLE_TABLEJOINEDTABLE_PER_CLASS结合软件设计模式(如模板、状态、策略、访问者等)。).将继承策略与软件设计模式结合起来是利用 JPA 继承的最佳选择。

  • 读写速度很快

  • 只要基类不需要成为一个实体,那么@MappedSuperclass就是每类一张表继承策略的合适替代

    缺点:

完整的代码可以在 GitHub 11 上找到。

正如在SINGLE_TABLE, JOINEDTABLE_PER_CLASS的情况下,我们可以通过创建一个由具体存储库扩展的基本存储库来避免查询方法的重复。GitHub 12 上有完整的例子。

Footnotes 1

hibernate pringb 单表阳性银屑病 NZ

  2

hibernate pringb bootdatabasetrig ers

  3

hibernate pringb otsingletablein heritage

  4

hibernate pringb oot 规范 sub assfrominherit ce

  5

hibernate pringb otjointablerepo sitoryinheritac e

  6

hibernate pringb otjointableinritz

  7

hibernate pringb otjoinedandrstra tey

  8

hibernate pringb otjoinedandvisi tor

  9

hibernate pringb boottableper 让 repository inheres tance

  10

hibernate pringb boottableper class 继承

  11

hibernate pringb oomappedsupecl ass

  12

hibernate pringb othmappedsupecl assrepository

 

十六、类型和 Hibernate 类型

第 143 项:如何通过 Hibernate 类型库处理 Hibernate 和不支持的类型

根据经验,努力选择最佳的数据库列类型。慢慢来,滚动您的数据库类型,因为大多数数据库都有您可以使用的特定类型。例如,MySQL 的MEDIUMINT UNSIGNED存储 1 到 99999 范围内的整数,PostgreSQL 的money类型存储具有固定小数精度的货币金额,cidr类型保存 IPv4 或 IPv6 网络规范,等等。而且,尽量使用紧凑类型。这将减少索引内存占用,并允许数据库操作更大量的数据。

可以把 Hibernate 类型想象成 Java 类型(对象或原语)和 SQL 类型之间的桥梁*。Hibernate ORM 自带了一组内置的受支持的类型,但是也有 Hibernate 不支持的其他 Java 类型(比如 Java 8 中引入的java.time.YearMonth)。*

*特别是对于不受支持的类型,您可以依赖 Hibernate 类型库。

Hibernate 类型库是由 Vlad Mihalcea 开发的开源项目,可以在 GitHub 1 上获得。我强烈建议您花几分钟时间来看看这个项目。你会喜欢的!

这个库提供了一套 Hibernate ORM 不支持的额外类型和实用程序。在这些类型中,有java.time.YearMonth。让我们通过 Hibernate 类型将这种类型存储在数据库中。首先,将依赖项添加到pom.xml文件中(对于 Maven):

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>2.4.3</version>
</dependency>

进一步,定义一个名为Book的实体。注意如何通过@TypeDef注释将java.time.YearMonth Java 类型映射到 Hibernate 的YearMonthIntegerType(或YearMonthDateType)类型:

import org.hibernate.annotations.TypeDef;
import com.vladmihalcea.hibernate.type.basic.YearMonthIntegerType;
...
@Entity
@TypeDef(
    typeClass = YearMonthIntegerType.class, // or, YearMonthDateType
    defaultForType = YearMonth.class
)

public class Book implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String isbn;
    private YearMonth releaseDate;

    // getters and setters omitted for brevity
}

最后,服务方法可以帮助您在数据库中持久化一个Book实例:

public void newBook() {

    Book book = new Book();

    book.setIsbn("001");
    book.setTitle("Young Boy");
    book.setReleaseDate(YearMonth.now());

    bookRepository.save(book);
}

图 16-1 显示数据库内容(查看release_date栏)。

img/487471_1_En_16_Fig1_HTML.jpg

图 16-1

发布日期列

以及获取这个Book的服务方法:

public void displayBook() {
    Book book = bookRepository.findByTitle("Young Boy");

    System.out.println(book);
}

以下是输出:

Book{id=1, title=Young Boy, isbn=001, releaseDate=2019-07}

完整的代码可以在 GitHub 2 上找到。

项目 144:如何映射区块和斑点

我们来看一下Author实体。在它的属性中,一个作者可以有一个avatar(一张照片)和一个biography(几页文本)。avatar可以认为是二进制大对象 (BLOB),而biography可以认为是字符大对象 (CLOB)。映射二进制/字符大对象是在易用性性能之间的权衡。

易用性(权衡性能)

遵循 JPA 规范,二进制大对象可以映射到byte[],而字符大对象可以映射到String。让我们看看代码:

@Entity
public class Author implements Serializable {

    ...
    @Lob
    private byte[] avatar;

    @Lob
    private String biography;
    ...

    public byte[] getAvatar() {
        return avatar;
    }

    public void setAvatar(byte[] avatar) {
        this.avatar = avatar;
    }

    public String getBiography() {
        return biography;
    }

    public void setBiography(String biography) {
        this.biography = biography;
    }
    ...
}

持久化和获取avatarbiography很容易,如下面的服务方法(假设findByName()AuthorRepository中的一个查询方法,要持久化的数据存储在两个本地文件中):

public void newAuthor() throws IOException {

    Author mt = new Author();
    mt.setName("Martin Ticher");
    mt.setAge(43);
    mt.setGenre("Horror");

    mt.setAvatar(Files.readAllBytes(
        new File("avatars/mt_avatar.png").toPath()));
    mt.setBiography(Files.readString(
        new File("biography/mt_bio.txt").toPath()));

    authorRepository.save(mt);
}

public void fetchAuthor() {

    Author author = authorRepository.findByName("Martin Ticher");

    System.out.println("Author bio: "
        + author.getBiography());
    System.out.println("Author avatar: "
        + Arrays.toString(author.getAvatar()));
}

将二进制/字符大对象映射到byte[]String很容易,但是可能会带来性能损失。当您获取二进制/字符大对象时,您正在获取所有信息并将其映射到一个 Java 对象。这会导致性能下降,尤其是当信息量非常大时(例如,视频、高清图像、音频等)。).在这种情况下,最好依靠 JDBC 的高球定位器java.sql.Clobjava.sql.Blob,如下所述。

完整的应用可在 GitHub 3 上获得。

避免性能损失(权衡是易用性)

通过 JDBC 的 LOB 定位器ClobBlob映射二进制/字符大对象支持 JDBC 驱动程序优化,例如数据流。实体映射非常简单:

@Entity
public class Author implements Serializable {

    ...
    @Lob
    private Blob avatar;

    @Lob
    private Clob biography;
    ...

    public Blob getAvatar() {
        return avatar;
    }

    public void setAvatar(Blob avatar) {
        this.avatar = avatar;
    }

    public Clob getBiography() {
        return biography;
    }

    public void setBiography(Clob biography) {
        this.biography = biography;
    }
    ...
}

虽然实体映射非常容易,但是持久化和获取二进制/字符大对象需要 Hibernate 特有的BlobProxyClobProxy类以及一些 I/O 代码。创建BlobClob需要这些类。下面的服务方法揭示了如何保存和获取avatarbiography:

public void newAuthor() throws IOException {

    Author mt = new Author();
    mt.setName("Martin Ticher");
    mt.setAge(43);
    mt.setGenre("Horror");

    mt.setAvatar(BlobProxy.generateProxy(
        Files.readAllBytes(new File("avatars/mt_avatar.png").toPath())));
    mt.setBiography(ClobProxy.generateProxy(
        Files.readString(new File("biography/mt_bio.txt").toPath())));

    authorRepository.save(mt);
}

public void fetchAuthor() throws SQLException, IOException {

    Author author = authorRepository.findByName("Martin Ticher");

    System.out.println("Author bio: "
        + readBiography(author.getBiography()));
    System.out.println("Author avatar: "
        + Arrays.toString(readAvatar(author.getAvatar())));
}

private byte[] readAvatar(Blob avatar) throws SQLException, IOException {

    try (InputStream is = avatar.getBinaryStream()) {

        return is.readAllBytes();
    }
}

private String readBiography(Clob bio) throws SQLException, IOException {

    StringBuilder sb = new StringBuilder();
    try (Reader reader = bio.getCharacterStream()) {

        char[] buffer = new char[2048];
        for (int i = reader.read(buffer); i > 0; i = reader.read(buffer)) {

            sb.append(buffer, 0, i);
        }
    }

    return sb.toString();
}

完整的应用可在 GitHub 4 上获得。

如果二进制/字符大对象被急切地加载并且没有被使用/利用,那么处理它们会导致性能下降。例如,加载一个author不需要同时加载avatarbiography。该信息可以通过项 23项 24 中介绍的惰性属性加载技术按需加载。

对于民族化的字符数据类型(如NCLOBNCHARNVARCHARLONGNVARCHAR,将@Lob替换为@Nationalized,如下:

@Nationalized
private String biography;

第 145 项:如何有效地将 Java 枚举映射到数据库

考虑一下Author实体和genre属性。这个属性用一个 Java enum来表示,如下:

public enum GenreType {

    HORROR, ANTHOLOGY, HISTORY
}

现在,让我们看看将这个enum映射到数据库的几种方法。

通过 EnumType 映射。线

一个非常简单的方法是使用@Enumerated( EnumType.STRING),如下所示:

@Entity
public class Author implements Serializable {

    @Enumerated(EnumType.STRING)
    private GenreType genre;
    ...
}

但是这种方式的效率如何呢?在 MySQL 中,genre列将是一个VARCHAR(255)。显然,该列占据了过多的空间。现在怎么样?

@Enumerated(EnumType.STRING)
@Column(length = 9)
private GenreType genre;

九个字节的长度足以保存ANTHOLOGY值。这应该没问题,只要你没有几百万条记录。这不太可能,但是假设你有 1500 万个作者,仅genre一栏就需要 120+ MB。这样一点效率都没有!

通过 EnumType 映射。序数

为了提高效率,让我们从EnumType.STRING切换到EnumType.ORDINAL:

@Enumerated(EnumType.ORDINAL)
private GenreType genre;

这一次,在 MySQL 中,genre列将是类型int(11)。在 MySQL 中,INTEGER(或INT)类型需要四个字节。这比VARCHAR(9)好多了。最有可能的是,你不会有超过 100 个流派,所以TINYINT应该做这项工作:

@Enumerated(EnumType.ORDINAL)
@Column(columnDefinition = "TINYINT")
private GenreType genre;

在 MySQL 中,TINYINT只需要一个字节来表示-128 到 127 之间的值。在这种情况下,存储 1500 万作者将需要 14mb 以上的空间。

即便如此,在某些场景中,TINYINT可能是不够的。更大的范围,靠SMALLINT,需要两个字节,覆盖-32768 到 32767 之间的范围。不太可能有一个有这么多价值观的enum

总之,依靠EnumType.ORDINAL比依靠EnumType.STRING更有效。尽管如此,可读性是一个代价。

完整的应用可在 GitHub 5 上获得。

将枚举映射到自定义表示

默认情况下,使用EnumType.ORDINAL会将HORROR链接到0ANTHOLOGY链接到1、和HISTORY链接到2。但是,我们假设HORROR应该链接到10ANTHOLOGY20HISTORY30

一种将enum映射到自定义表示的方法依赖于AttributeConverter。我们在第 19 项中使用了一个AttributeConverter,所以下面的实现应该似曾相识:

public class GenreTypeConverter
                implements AttributeConverter<GenreType, Integer> {

    @Override
    public Integer convertToDatabaseColumn(GenreType attr) {

        if (attr == null) {
            return null;
        }

        switch (attr) {
            case HORROR:
                return 10;
            case ANTHOLOGY:
                return 20;
            case HISTORY:
                return 30;
            default:
                throw new IllegalArgumentException("The " + attr
                                                    + " not supported.");
        }
    }

    @Override
    public GenreType convertToEntityAttribute(Integer dbData) {

        if (dbData == null) {
            return null;
        }

        switch (dbData) {
            case 10:
                return HORROR;
            case 20:
                return ANTHOLOGY;
            case 30:
                return HISTORY;
            default:
                throw new IllegalArgumentException("The " + dbData
                                                    + " not supported.");
        }
    }
}

最后使用@Converter指令 Hibernate 应用转换器:

@Entity
public class Author implements Serializable {
    ...
    @Convert(converter = GenreTypeConverter.class)
    @Column(columnDefinition = "TINYINT")
    private GenreType genre;
    ...
}

完整的应用可在 GitHub 6 上获得。

将枚举映射到特定于数据库的枚举类型(PostgreSQL)

PostgreSQL 定义了一个可以通过CREATE TYPE命令使用的ENUM类型,如下例所示:

CREATE TYPE genre_info AS ENUM ('HORROR', 'ANTHOLOGY', 'HISTORY')

编写自定义类型

Hibernate 不支持这种类型(Hibernate 可以将enum值映射到一个int或一个String,但是 PostgreSQL 期望值是一个Object,所以将一个 Java enum映射到 PostgreSQL ENUM需要您实现一个定制的 Hibernate 类型。定义这个自定义 Hibernate 类型意味着您需要扩展 Hibernate EnumType并覆盖nullSafeSet()方法来形成所需的行为:

public class PostgreSQLEnumType extends EnumType {

    @Override
    public void nullSafeSet(PreparedStatement ps, Object obj, int index,
            SharedSessionContractImplementor session)
                throws HibernateException, SQLException {
        if (obj == null) {
            ps.setNull(index, Types.OTHER);
        } else {
            ps.setObject(index, obj.toString(), Types.OTHER);
        }
    }
}

最后,让我们用一个@TypeDef注释注册这个类型,并把它放在一个package-info.java文件中:

@org.hibernate.annotations.TypeDef(
    name = "genre_enum_type", typeClass = PostgreSQLEnumType.class)

package com.bookstore.type;

现在,让我们使用它:

@Entity
public class Author implements Serializable {
    ...
    @Enumerated(EnumType.STRING)
    @Type(type = "genre_enum_type")
    @Column(columnDefinition = "genre_info")
    private GenreType genre;
    ...
}

坚持一个作者揭示出他们的genre是类型genre_info,是一个 PostgreSQL ENUM,如图 16-2 。

img/487471_1_En_16_Fig2_HTML.jpg

图 16-2

PostgreSQL 枚举类型

完整的应用可在 GitHub 7 上获得。

使用 Hibernate 类型库

Hibernate 类型库是在项目 143 中引入的。幸运的是,这个库已经包含了 Java enum到 PostgreSQL ENUM类型的映射。首先,通过以下依赖项将该库添加到您的应用中:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>2.4.3</version>
</dependency>

然后,在实体类级别使用@TypeDef注释,在实体字段级别使用@Type,如下所示:

@Entity
@TypeDef(
    name = "genre_enum_type",
    typeClass = PostgreSQLEnumType.class
)
public class Author implements Serializable {
    ...
    @Enumerated(EnumType.STRING)
    @Type(type = "genre_enum_type")
    @Column(columnDefinition = "genre_info")
    private GenreType genre;
    ...
}

完整的应用可在 GitHub 8 上获得。

第 146 项:如何有效地将 JSON Java 对象映射到 MySQL JSON 列

JSON 非常适合非结构化数据。

MySQL 从 5.7 版本开始增加了 JSON 类型支持。然而,Hibernate Core 没有提供适用于 JSON Java Object和数据库 JSON 列的 JSON Type

幸运的是,Hibernate 类型库(你应该熟悉来自 Item 143 的这个库)填补了这个空白,并提供了两个通用的 JSON 类型——JsonStringTypeJsonBinaryType。在 MySQL 的情况下,从 JDBC 的角度来看,JSON 类型应该表示为String s,所以JsonStringType是正确的选择。

让我们使用Author实体和Book JSON Java Object。这里列出了Author实体:

@Entity
@TypeDef(
    name = "json", typeClass = JsonStringType.class
)
public class Author implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String genre;
    private int age;

    @Type(type = "json")
    @Column(columnDefinition = "json")
    private Book book;

    // getters and setters omitted for brevity
}

这里列出了Book JSON Java Object(这不是 JPA 实体):

public class Book implements Serializable {

    private static final long serialVersionUID = 1L;

    private String title;
    private String isbn;
    private int price;

    // getters and setters omitted for brevity
}

坚持作者

服务方法可以轻松地持久化一个作者,如下所示:

public void newAuthor() {

    Book book = new Book();
    book.setIsbn("001-JN");
    book.setTitle("A History of Ancient Prague");
    book.setPrice(45);

    Author author = new Author();
    author.setName("Joana Nimar");
    author.setAge(34);
    author.setGenre("History");
    author.setBook(book);

    authorRepository.save(author);
}

INSERT的说法是:

INSERT INTO author (age, book, genre, name)
  VALUES (34, '{"title":"A History of Ancient Prague",
                "isbn":"001-JN","price":45}', 'History', 'Joana Nimar')

author表如图 16-3 所示。

img/487471_1_En_16_Fig3_HTML.jpg

图 16-3

MySQL 中的 JSON

获取/更新作者

获取作者会将获取的 JSON 映射到Book对象。例如,考虑以下查询:

public Author findByName(String name);

调用findByName()会触发下面的SELECT语句:

Author author = authorRepository.findByName("Joana Nimar");

SELECT
  author0_.id AS id1_0_,
  author0_.age AS age2_0_,
  author0_.book AS book3_0_,
  author0_.genre AS genre4_0_,
  author0_.name AS name5_0_
FROM author author0_
WHERE author0_.name = ?

通过取出的author,可以调用getBook().getTitle()getBook().getIsbn()getBook().getPrice()。调用getBook().setTitle()getBook().setIsbn()getBook().setPrice()将触发 JSON 更新。这个UPDATE看起来是这样的(getBook().setPrice(40)):

UPDATE author
SET age = 34,
    book = '{"title":"A History of Ancient Prague",
             "isbn":"001-JN","price":40}',
    genre = 'History',
    name = 'Joana Nimar'
WHERE id = 1

通过查询 JSON 获取作者

MySQL 提供了基于给定的路径表达式提取部分或修改 JSON 文档的函数。其中一个功能是JSON_EXTRACT()。它获得两个参数:要查询的 JSON 和一个路径表达式。path 语法依赖于一个前导字符$来表示 JSON 文档,这个字符后面可选地跟着表示文档某些部分的连续选择器。有关更多细节,请查看 MySQL 文档 9

WHERE子句中调用JSON_EXTRACT()可以通过 JPQL function()或本地查询来完成。通过 JPQL,它看起来像下面的例子(这找到了用给定的isbn写这本书的作者):

@Query("SELECT a FROM Author a "
     + "WHERE function('JSON_EXTRACT', a.book, '$.isbn') = ?1")
 public Author findByBookIsbn(String isbn);

或者,作为本机查询:

@Query(value = "SELECT a.* FROM author a
                WHERE JSON_EXTRACT(a.book, '$.isbn') = ?1",
       nativeQuery = true)
public Author findByBookIsbnNativeQuery(String isbn);

调用JSON_EXTRACT()(以及其他 JSON 特有的函数如JSON_SET()JSON_MERGE_FOOJSON_OBJECT()等。)在SELECT部分的查询可以通过原生查询或注册函数来完成,如第 79 项所示。

完整的应用可在 GitHub 10 上获得。

第 147 项:如何有效地将 JSON Java 对象映射到 PostgreSQL JSON 列

第 146 项涵盖了 MySQL JSON 类型。现在,让我们来关注一下 PostgreSQL。

PostgreSQL 从 9.2 版开始增加了 JSON 类型支持。PostgreSQL JSON 类型有jsonjsonb。PostgreSQL JSON 类型以二进制数据格式表示,所以需要使用JsonBinaryType(在第 146 项中,我们说过 Hibernate 类型库提供了两种通用的 JSON 类型——JsonStringTypeJsonBinaryType)。

让我们使用Author实体和Book JSON Java Object。这里列出了Author实体:

@Entity
@TypeDef(
    name = "jsonb", typeClass = JsonBinaryType.class
)
public class Author implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String genre;
    private int age;

    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb") // or, json
    private Book book;

    // getters and setters omitted for brevity
}

这里列出了Book JSON Java Object(这不是 JPA 实体):

public class Book implements Serializable {

    private static final long serialVersionUID = 1L;

    private String title;
    private String isbn;
    private int price;

    // getters and setters omitted for brevity
}

坚持作者

服务方法可以轻松地持久化一个作者,如下所示:

public void newAuthor() {

    Book book = new Book();
    book.setIsbn("001-JN");
    book.setTitle("A History of Ancient Prague");
    book.setPrice(45);

    Author author = new Author();
    author.setName("Joana Nimar");
    author.setAge(34);
    author.setGenre("History");
    author.setBook(book);

    authorRepository.save(author);
}

INSERT的说法是:

INSERT INTO author (age, book, genre, name)
  VALUES (34, '{"title":"A History of Ancient Prague",
                "isbn":"001-JN","price":45}', 'History', 'Joana Nimar')

author表如图 16-4 所示。

img/487471_1_En_16_Fig4_HTML.jpg

图 16-4

PostgreSQL 中的 JSON

获取/更新作者

获取作者会将获取的 JSON 映射到Book对象。例如,考虑以下查询:

public Author findByName(String name);

调用findByName()会触发下面的SELECT语句:

Author author = authorRepository.findByName("Joana Nimar");

SELECT
  author0_.id AS id1_0_,
  author0_.age AS age2_0_,
  author0_.book AS book3_0_,
  author0_.genre AS genre4_0_,
  author0_.name AS name5_0_
FROM author author0_
WHERE author0_.name = ?

通过取出的author,可以调用getBook().getTitle()getBook().getIsbn()getBook().getPrice()。调用getBook().setTitle()getBook().setIsbn()getBook().setPrice()将触发 JSON 更新。这个UPDATE看起来是这样的(getBook().setPrice(40)):

UPDATE author
SET age = 34,
    book = '{"title":"A History of Ancient Prague",
             "isbn":"001-JN","price":40}',
    genre = 'History',
    name = 'Joana Nimar'
WHERE id = 1

通过查询 JSON 获取作者

PostgreSQL 提供了两个用于查询 JSON 数据的本地操作符(更多详细信息请参见 PostgreSQL 文档 11 ):

  • ->操作符通过键返回 JSON 对象字段

  • ->>操作符通过文本返回 JSON 对象字段

作为本地操作符,它们必须在本地查询中使用。例如,获取用给定的 ISBN 写了一本书的作者的方法如下:

@Query(value = "SELECT a.* FROM author a "
             + "WHERE a.book ->> 'isbn' = ?1",
      nativeQuery = true)
public Author findByBookIsbnNativeQuery(String isbn);

有时,您需要将 JSON 字段转换为适当的数据类型。例如,要将图书的price包含在比较中,必须将其转换为INTEGER,如下所示:

@Query(value = "SELECT a.* FROM author a "
             + "WHERE CAST(a.book ->> 'price' AS INTEGER) = ?1",
      nativeQuery = true)
public Author findByBookPriceNativeQueryCast(int price);

完整的应用可在 GitHub 12 上获得。

Footnotes 1

https://github.com/vladmihalcea/hibernate-types

  2

hibernate pringb 欧塔年月

  3

hibernate pringb ootbmapping lobtob 字节字符串

  4

hibernate pringb ootbmapping lobtoc 赞美诗

  5

hibernate pringb otenumstringint

  6

hibernate pringb ootpointattribute converter

  7

hibernate pringb ootpenumpostgreq lccustomtype

  8

hibernate pringb ootpointresq lhiberinatetype

  9

https://dev.mysql.com/doc/refman/8.0/en/json.html

  10

hibernate pringb oojsontomysql

  11

https://www.postgresql.org/docs/9.4/datatype-json.html

  12

hibernate pringb oojsontoposter SQL

 

*