preface

The project I am working on now uses Spring Data Jpa. I used to use Mybatis. I studied the use of Jpa some time ago.

The current structure of the company’s project has many technical points that I have not practiced, so I am learning these things these days. I will try to update my blog once a week since 2021-5-15.

Update GraphQL next week.

The examples in this article are all at github.com/zhangpanqin… The database uses an in-memory database H2.

JPA

Know the JPA

JPA, short for the Java Persistence API, defines the mapping between Java objects and database tables, as well as the interface specification that defines how CRUD should work at runtime.

Hibernate provides an implementation of JPA. There are other implementations, such as Open Jpa and so on.

Spring Data provides a familiar and consistent spring-based programming model for Data access while retaining the special characteristics of the underlying Data store.

  • Spring Data JPA is used to manipulate relational databases

  • Spring Data MongoDB Is used to operate MongoDB

  • Spring Data Elasticsearch is used to manipulate Es

  • Spring Data Redis is used to operate Redis

The underlying JPA implementation of Spring Data JPA adopts Hibernate, or it can be said to encapsulate Hibernate, providing a unified programming model of Spring.

The unified programming model is: the following code can operate on JPA, ES, Redis, etc., but the annotation on Person is different.

The CrudRepository interface can also be replaced to provide more fine-grained data control for different databases.

public interface PersonRepository extends CrudRepository<Person.Long> {

  List<Person> findByLastname(String lastname);

  List<Person> findByFirstnameLike(String firstname);
}
Copy the code

Introduction to JPA common annotations

Do not use a database foreign key when using JPA because it affects performance and is not conducive to database replacement.

Instead of using Hibernate to generate table structures, use flyway components to control database tables, indexes, and field management through SQL. Flyway is more flexible

@Data
@Entity
@Table(name = "sys_user")
public class SysUserEntity extends BaseEntity {

    private String nickname;

    private Integer age;

    ReferencedColumnName is the primary key of the current table */
    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = USER_ID,referencedColumnName = ID,foreignKey =@ForeignKey(NO_CONSTRAINT) )
    private List<SysBlogEntity> sysBlogEntities;
}
Copy the code

@Entity

Flags the class as an Entity and is managed by JPA

@Table

Specify that Entity maps to that table in the database

@JoinColumn

Specifies which two fields are associated between two associated tables

@Column

Specifies that the Entity field is associated with that field in the table

@Id

Specify the primary key field

@GeneratedValue

Specify the primary key generation strategy, as detailed below

@Transient

Ignore the mapping between fields and table fields

@OneToMany

The one-to-many relationship specifies the mapping relationship between the current Entity and another Entity.

ReferencedColumnName is the primary key of the current table */
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = USER_ID,referencedColumnName = ID,foreignKey =@ForeignKey(NO_CONSTRAINT) )
private List<SysBlogEntity> sysBlogEntities;
Copy the code

@ManyToOne

Reference OneToMany

@ManyToMany

/ * * *@JoinTableSpecify the intermediate table, and the field mapping * in the intermediate table@JoinColumn(name = ROLE_ID,referencedColumnName = ID) Specifies that the intermediate columnname is associated with that other columnname */
@ManyToMany
@JoinTable(name = SYS_USER_ROLE, joinColumns = {@JoinColumn(name = USER_ID, referencedColumnName = ID)}, inverseJoinColumns = {@JoinColumn(name = ROLE_ID,referencedColumnName = ID)})
private List<SysRoleEntity> sysRoleEntities;
Copy the code

@OneToOne

@OneToOne(optional=false)
@JoinColumn(name="CUSTREC_ID", unique=true, nullable=false, updatable=false)
private CustomerRecord customerRecord;
Copy the code

@Query

Can write SQL operation database

public interface SysBlogRepository extends JpaRepository<SysBlogEntity.Long> {
    @Query(nativeQuery = true,value = "select * from sys_blog where user_id = :userId")
    List<SysBlogEntity> findByUserId(Long userId);

    @Query(nativeQuery = true ,value = "select * from sys_blog where title = :#{#sysBlogDTO.title}")
    List<SysBlogEntity> findByTitle(@Param("sysBlogDTO") SysBlogDTO sysBlogDTO);
}
Copy the code

Primary key generation policy

/** * strategy The value ** AUTO is controlled by the program and the default policy. SQL > alter table IDENTITY * * alter table IDENTITY * * alter table IDENTITY * * alter table IDENTITY * * alter table IDENTITY * * alter table IDENTITY * * alter table IDENTITY * * alter table IDENTITY * * Oracle, PostgreSQL, and DB2 can use * to use sequences as primary keys@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "emailSeq")
 * @SequenceGenerator(initialValue = 1, name = "emailSeq", sequenceName = "EMAIL_SEQUENCE") * private long id; Create sequence EMAIL_SEQUENCE; * when sequenceName is not specified in SequenceGenerator, Hibernate provides a hibernate_sequence * * TABLE sequenceName by default
public @interface GeneratedValue {
    GenerationType strategy(a) default AUTO;
    String generator(a) default "";
}
Copy the code

Example of primary key generation strategy

@Data
@Table
@Entity
public class KeyGeneratorEntity {
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid",strategy = "uuid")
    private String id;

    private String username;
}

@SpringBootTest
class KeyGeneratorRepositoryTest {

    @Autowired
    private KeyGeneratorRepository keyGeneratorRepository;

    @Test
    public void run(a){
        final KeyGeneratorEntity keyGeneratorEntity = new KeyGeneratorEntity();
        keyGeneratorEntity.setUsername(LocalDateTime.now().toString());
        keyGeneratorRepository.save(keyGeneratorEntity);

        final List<KeyGeneratorEntity> all = keyGeneratorRepository.findAll();
        / / [KeyGeneratorEntity (id = ff808081796f260c01796f2616aa0000, username = 2021-05-15 T16: incense which. 722)]System.out.println(all); }}Copy the code

Hibernate provides the following primary key generation strategies

When @ GeneratedValue (strategy = GenerationType. SEQUENCE) is using SequenceStyleGenerator. Class control primary key generation.

When @GenericGenerator(name = “system-uuid”,strategy = “uuid”), uuidhexGenerator.class is used

public class DefaultIdentifierGeneratorFactory		implements MutableIdentifierGeneratorFactory.Serializable.ServiceRegistryAwareService {	private ConcurrentHashMap<String, Class> generatorStrategyToClassNameMap = new ConcurrentHashMap<String, Class>();	@SuppressWarnings("deprecation")	public DefaultIdentifierGeneratorFactory(a) {		register( "uuid2", UUIDGenerator.class );		register( "guid", GUIDGenerator.class );			// can be done with UUIDGenerator + strategy		register( "uuid", UUIDHexGenerator.class );			// "deprecated" for new use		register( "uuid.hex", UUIDHexGenerator.class ); 	// uuid.hex is deprecated		register( "assigned", Assigned.class );		register( "identity", IdentityGenerator.class );		register( "select", SelectGenerator.class );		register( "sequence", SequenceStyleGenerator.class );		register( "seqhilo", SequenceHiLoGenerator.class );		register( "increment", IncrementGenerator.class );		register( "foreign", ForeignGenerator.class );		register( "sequence-identity", SequenceIdentityGenerator.class );		register( "enhanced-sequence", SequenceStyleGenerator.class );		register( "enhanced-table", TableGenerator.class );	}	public void register(String strategy, Class generatorClass) {		LOG.debugf( "Registering IdentifierGenerator strategy [%s] -> [%s]", strategy, generatorClass.getName() );		final Class previous = generatorStrategyToClassNameMap.put( strategy, generatorClass );		if ( previous != null ) {			LOG.debugf( "    - overriding [%s]", previous.getName() );		}	}}
Copy the code

Lazy needs to be executed within a transaction

public class Order1 {    @Id    @GeneratedValue    private Long id;    private String description;    @OneToMany(fetch = FetchType.LAZY)    @JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))    privateList<OrderItem> orderItemList; }// @Transactional(readOnly = true)public List
      
        listOrder(){ System. The out. Println (" -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - began to query -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - "); final List
       
         all = orderRepository.findAll(); System. The out. Println (" -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - begin to lazy loading -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - "); System.out.println(JSON.toJSONString(all)); return all; }
       
      
Copy the code

JPA does not query Lazy data when the data needs to be loaded lazily. It queries Lazy data only when it is used, but when used in the same transaction as the original query, otherwise it will throw the following exception

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.mflyyou.jpa.n1.Order1.orderItemList, could not initialize proxy - no Session
Copy the code

Order1.orderitemlist failed because the transaction was not opened and the query transaction was closed after orderRepository.findAll() was executed.

When the Transactional annotation @Transactional is added, the whole method executes within a transaction and no errors are reported.

N + 1 problem

@Entity@Table(name = "order1")@Datapublic class Order1 {    @Id    @GeneratedValue    private Long id;    private String description;    @OneToMany(fetch = FetchType.LAZY)    @JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))    privateList<OrderItem> orderItemList; }@Transactional(readOnly = true)public Order1 findOne(Long id){    System.out.println("--------------------- Start query ---------------------");    final Optional<Order1> byId = orderRepository.findById(id);    System.out.println("--------------------- lazy loading ---------------------");    System.out.println(JSON.toJSONString(byId.get()));    returnbyId.get(); }Copy the code

When Order1 is queried, orderItemList is not actually queried. It is queried once when orderItemList is used.

When Order1 has N associated attributes, it will query N times to get the corresponding data.

LAZY loading problems occur when data is retrieved in fetchtype. LAZY

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - start query -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- began to query -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - Hibernate: select order1x0_.id as id1_2_0_, order1x0_.description as descript2_2_0_ from order1 order1x0_ where Order1x0_. Id =? -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - begin to lazy loading -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - Hibernate: select orderiteml0_.order_id as order_id3_3_0_, orderiteml0_.id as id1_3_0_, orderiteml0_.id as id1_3_1_, orderiteml0_.name as name2_3_1_, orderiteml0_.order_id as order_id3_3_1_, orderiteml0_.price as price4_3_1_ from order_item orderiteml0_ whereCopy the code
{    "description": "Testing the 2021-05-15 T17:35:50. 349"."id": 1."orderItemList": [{"id": 2."name": "Ceshi2021-05-15 t17:35:50. 423"."orderId": 1."price": 10        },        {            "id": 3."name": "Ceshi2021-05-15 t17:35:50. 423"."orderId": 1."price": 10        },        {            "id": 4."name": "Ceshi2021-05-15 t17:35:50. 423"."orderId": 1."price": 10        },        {            "id": 5."name": "Ceshi2021-05-15 t17:35:50. 423"."orderId": 1."price": 10        },        {            "id": 6."name": "Ceshi2021-05-15 t17:35:50. 423"."orderId": 1."price": 10}}]Copy the code

Instead, use @onetomany (fetch = fetchType.eager).

The query gets all the data in one go

Hibernate:     select        order1x0_.id as id1_2_0_,        order1x0_.description as descript2_2_0_,        orderiteml1_.order_id as order_id3_3_1_,        orderiteml1_.id as id1_3_1_,        orderiteml1_.id as id1_3_2_,        orderiteml1_.name as name2_3_2_,        orderiteml1_.order_id as order_id3_3_2_,        orderiteml1_.price as price4_3_2_     from        order1 order1x0_     left outer join        order_item orderiteml1_             on order1x0_.id=orderiteml1_.order_id     where        order1x0_.id=?
Copy the code

@onetomany (fetch = FetchType.EAGER) error if orderItemList and orderItemList2 are present

public class Order1 {    @Id    @GeneratedValue    private Long id;    private String description;    @OneToMany(fetch = FetchType.EAGER)    @JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))    private List<OrderItem> orderItemList;    @OneToMany(fetch = FetchType.EAGER)    @JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))    privateList<OrderItem2> orderItemList2; }Copy the code

@NamedEntityGraph and @EntityGraph can solve the N+1 problem. Can also solve the cascading query, query which member variables, do not query which member variables. It gives us greater freedom to query data based on business.

EntityGraph

@NamedEntityGraph defines which data to query when querying. @EntityGraph is used to mark which NamedEntityGraph is used by Repository.

@Entity@Table(name = "order1")@Data@NamedEntityGraph(name = "searchOrderGraphItem", attributeNodes = { @NamedAttributeNode(value = "orderGraphItemList", subgraph = "OrderGraphItem_productGraphs"), }, subgraphs = { @NamedSubgraph(name = "OrderGraphItem_productGraphs", attributeNodes = { @NamedAttributeNode(value = "productGraphs") }) })public class OrderGraph1 {    @Id    @GeneratedValue    private Long id;    private String description;    @OneToMany(fetch = FetchType.EAGER)    @JoinColumn(name = "order_id", referencedColumnName = "id", foreignKey = @ForeignKey(NO_CONSTRAINT))    private Set<OrderGraphItem> orderGraphItemList;    @OneToMany(fetch = FetchType.EAGER)    @JoinColumn(name = "order_id", referencedColumnName = "id", foreignKey = @ForeignKey(NO_CONSTRAINT))    privateSet<OrderGraphItem2> orderGraphItemList2; }public interface OrderGraphRepository extends JpaRepository<OrderGraph1.Long> {    @EntityGraph(value = "searchOrderGraphItem", type = EntityGraph.EntityGraphType.FETCH)    OrderGraph1 findByIdEquals(Long id); }Copy the code
@Testpublic void findById(a) {    final OrderGraph1 orderGraph1s = orderGraphService.findById(1L); assertThat(orderGraph1s, notNullValue()); }Copy the code

The type specified in @entitygraph can FETCH and LOAD

  • The FETCH forNamedEntityGraphThe definition of theattributeNodesUse eager and lazy if undeclared
@EntityGraph(value = "searchOrderGraphItem", type = EntityGraph.EntityGraphType.FETCH)OrderGraph1 findByIdEquals(Long id);
Copy the code

Only the fields in the table corresponding to OrderGraph1 and orderGraphItemList are queried. OrderGraphItemList2 will only be queried when used.

  • The LOAD forNamedEntityGraphThe definition of theattributeNodes Use eager, undeclared properties configured using propertiesFetchType
@EntityGraph(value = "searchOrderGraphItem", type = EntityGraph.EntityGraphType.LOAD)OrderGraph1 findByIdEquals(Long id);
Copy the code

This will query the fields in the table corresponding to OrderGraph1 and orderGraphItemList. The orderGraphItemList2 property is also checked directly because FetchType.EAGER is configured.

OrderGraphItemList2 property is only detected when orderGraphItemList2 is used if fetchType. Lazy is configured

The audit function

You’ll see in a typical table, the primary key ID, the creation time, the update event, who created it, who updated it, plus the optimistic lock.

Implement AuditorAware and populate the user ID.

@Data@MappedSuperclass@EntityListeners(AuditingEntityListener.class)public abstract class BaseEntity {    @Id    private Long id;    /** * create time */    @CreatedDate    @Column(name = "create_date", updatable = false)    private Instant createDate;    /** * Change the time */    @LastModifiedDate    @Column(name = "update_date")    private Instant updateDate;    /** * who created */    @CreatedBy    @Column(name = "create_by", updatable = false)    private Integer createBy;    /** ** by whom */    @LastModifiedBy    @Column(name = "update_by")    private Integer updateBy;    /** * optimistic lock */    @Version    @Column(name = "version")    private Long version = 0L; }@Componentpublic class MyAuditorAware implements AuditorAware<Integer> {    /** * Get the current login id */    @Override    public Optional<Integer> getCurrentAuditor(a) {        Id return optional. ofNullable(100); }}
Copy the code

Optimistic lock, the updated version must be equal to the version in the database, otherwise the update will throw an exception. Can also use the Spring – retry capture ObjectOptimisticLockingFailureException retry the update.

@Data@Entity@Table(name = "sys_user")public class SysUserEntity extends BaseEntity {    private String nickname;    privateInteger age; }Copy the code
@SpringBootTestclass JpaStudyApplicationTests {    private static final Long USER_ID_EQUALS_1 = 1L;    private static final Long USER_ID_EQUALS_2 = 2L;    private static final Long USER_ID_EQUALS_3 = 3L;    @Resource    private SysUserRepository sysUserRepository;    private SysUserEntity saveSysUserEntity;    private SysUserEntity saveSysUserEntity2;    private SysUserEntity saveSysUserEntity3;    @BeforeEach    public void beforeEach(a) {        saveSysUserEntity = new SysUserEntity();        saveSysUserEntity.setAge(10);        saveSysUserEntity.setId(USER_ID_EQUALS_1);        saveSysUserEntity.setNickname("Test");        saveSysUserEntity.setVersion(10L);        saveSysUserEntity2 = new SysUserEntity();        saveSysUserEntity2.setAge(10);        saveSysUserEntity2.setId(USER_ID_EQUALS_2);        saveSysUserEntity2.setNickname("Test");        saveSysUserEntity2.setVersion(10L);        saveSysUserEntity3 = new SysUserEntity();        saveSysUserEntity3.setAge(10);        saveSysUserEntity3.setId(USER_ID_EQUALS_3);        saveSysUserEntity3.setNickname("Test");        saveSysUserEntity3.setVersion(10L);    }    @Test    public void should_update_error(a) {        sysUserRepository.save(saveSysUserEntity);        final Optional<SysUserEntity> byId = sysUserRepository.findById(USER_ID_EQUALS_1);        assertThat(byId.isPresent(), is(Boolean.TRUE));        final SysUserEntity sysUserEntity = byId.get();        // sysuserEntity.setVersion (2L); sysUserEntity.setVersion(12L); SysUserEntity. SetNickname (" optimistic locking update "+ LocalDateTime. Now (). The toString ()); final Executable executable = () -> sysUserRepository.save(sysUserEntity); assertThrows(ObjectOptimisticLockingFailureException.class, executable); } @Test public void should_update_success() { sysUserRepository.save(saveSysUserEntity2); Optional
      
        byId = sysUserRepository.findById(USER_ID_EQUALS_2); assertThat(byId.isPresent(), is(Boolean.TRUE)); SysUserEntity sysUserEntity = new SysUserEntity(); sysUserEntity.setId(USER_ID_EQUALS_2); sysUserEntity.setVersion(10L); SysUserEntity. SetNickname (" optimistic locking update "+ LocalDateTime. Now (). The toString ()); SysUserEntity save = sysUserRepository.save(sysUserEntity); assertThat(save.getVersion(), is(11L)); }}
      
Copy the code