从零一起学Spring Boot之LayIM项目长成记(四) Spring Boot JPA 深入了解
前言
本篇内容主要是一些关于JPA的常用的一些用法等。内容也是很多是看其他博客学来的,顺道在本系列博客里抽出一篇作为总结。下面让我们来看看吧。
不过我更推荐大家读本篇:https://lufficc.com/blog/spring-boot-jpa-basic
@Entity注解
标注在实体类上,每个实体类映射数据库里的一张表,name属性定义表名。默认表名为驼峰变量之间加下划线。例如:ProductInfo,生成的表名为product_info.
@Entity(name="userinfo") //default user_info public class UserInfo{ }
@Table注解
标注在实体类上,用于定义生成的表的相关属性。name属性定义表名。
@Entity @Table(name="userinfo") public class UserInfo{ }
@MappedSuperclass
标注在类上,但是不会映射到数据库中的表。他的作用就是作为实体类通用的部分,比如id,status,create_time,update_time等通用字段。集成该类的实体生成的数据库字段中包含带有@MappedSuperclass注解的字段。
@MappedSuperclass public class DomainBase{ @Id @GenerateValue protected Long id; protected Long create_time; //getter setter }
@Entity(name="userinfo")
@AttributeOverride(name="create_time",column =@column(name="create_at"))
//@AttributeOverrides({...}) public class UserInfo extends DomainBase{ private String name; private Integer age; //getter setter }
生成的表(userinfo)中除了name 和 age 字段以外,还有DomainBase中的id和create_time字段。不过子类中还可以用@AttributeOverride属性重新定义create_time 或者 id。
@Basic注解
这个注解不太常用,因为实体属性默认注解就是@Basic。
@Entity public class UserInfo{ @Basic(optional=false,fetch=FetchType.LAZY) private String name; }
@Id注解
这个没有什么好说的,就是主键。length定义长度。
@GeneratedValue注解
strategy:主键生成策略。可选值:
GenerationType.TABLE
GenerationType.SEQUENCE
GenerationType.IDENTITY
GenerationType.AUTO
默认是 GenerationType.AUTO。
generator:主键生成器名称
@Entity(name="userinfo") public class UserInfo{ @Id @GenerateValue private Long id; }
@Transient注解
带有该注解的字段不会再数据库中创建。
@Temporal注解
日期类型的字段
@Entity(name = "user") public class User implements Serializable { @Id @GeneratedValue private Long id; @Temporal(TemporalType.DATE) private Date createDate; @Temporal(TemporalType.TIMESTAMP) private Date loginTime; @Temporal(TemporalType.TIME) private Date expireTime; // getters and setters }
@Column注解
这个表格 from http://fanlychie.github.io/post/jpa-column-annotation.html
参数 | 类型 | 描述 |
---|---|---|
name | String | 列的名称,默认为属性的名称(Hibernate 映射列时,若遇到驼峰拼写,会自动添加 _ 连接并将大写字母改成小写)。 |
unique | boolean | 列的值是否是唯一的。这是 @UniqueConstraint 注解的一个快捷方式, 实质上是在声明唯一约束。默认值为 false。 |
nullable | boolean | 列的值是否允许为 null。默认为 true。 |
insertable | boolean | 列是否包含在 INSERT 语句中,默认为 true。 |
updatable | boolean | 列是否包含在 UPDATE 语句中,默认为 true。 |
columnDefinition | String | 生成列的 DDL 时使用的 SQL 片段。默认使用推断的类型来生成 SQL 片段以创建此列。 |
table | String | 当前列所属的表的名称。 |
length | int | 列的长度,仅对字符串类型的列生效。默认为255。 |
precision | int | 列的精度,仅对十进制数值有效,表示有效数值的总位数。默认为0。 |
scale | int | 列的精度,仅对十进制数值有效,表示小数位的总位数。默认为0。 |
@OneToOne注解
一对一关联,比如一个用户只有一张身份证。
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; private String name; @OneToOne private IdCard idCard; }
@Entity public class IdCard { @Id @GeneratedValue private long id; private String address; private String province;
}
生成的SQL如下:
CREATE TABLE `id_card` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `address` varchar(255) DEFAULT NULL, `city` varchar(255) DEFAULT NULL, `province` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `id_card_id` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`), KEY `FK2y8xk8loi3isby5ju3eg7xan1` (`id_card_id`), CONSTRAINT `FK2y8xk8loi3isby5ju3eg7xan1` FOREIGN KEY (`id_card_id`) REFERENCES `id_card` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
可以看到,带有@OneToOne注解的idcard字段生成了 id_card_id. 当然我们可以在加上 @JoinColumn(name = "card_id") 自定义关联列名
@OneToMany注解
一对多关联,比如一个用户对应多个邮寄地址
@Entity public class Address { @Id private long id; private String detail; private long uid; }
@OneToMany @JoinColumn(name = "uid") private List<Address> addresses;
生成的SQL语句如下:
CREATE TABLE `address` ( `id` bigint(20) NOT NULL, `detail` varchar(255) DEFAULT NULL, `uid` bigint(20) NOT NULL, PRIMARY KEY (`id`), KEY `FKbfn1l96xkepprn0hffrwr99dl` (`uid`), CONSTRAINT `FKbfn1l96xkepprn0hffrwr99dl` FOREIGN KEY (`uid`) REFERENCES `user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@ManyToOne注解
同理,多个邮寄地址对应一个用户,Address类修改如下:
@Entity public class Address { @Id private long id; private String detail; @ManyToOne @JoinColumn(name="uid") private User user; }
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; private String name; @OneToOne @JoinColumn(name = "card_id") private IdCard idCard; @OneToMany(mappedBy = "user") private List<Address> addresses; }
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `card_id` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`), KEY `FK7nybup9cdqe2mh98vfnd035gd` (`card_id`), CONSTRAINT `FK7nybup9cdqe2mh98vfnd035gd` FOREIGN KEY (`card_id`) REFERENCES `id_card` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `address` ( `id` bigint(20) NOT NULL, `detail` varchar(255) DEFAULT NULL, `uid` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`), KEY `FKbfn1l96xkepprn0hffrwr99dl` (`uid`), CONSTRAINT `FKbfn1l96xkepprn0hffrwr99dl` FOREIGN KEY (`uid`) REFERENCES `user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@ManyToMany注解
多对多关联。一个用户对应多个标签,每个标签对应多个用户。如果只是用户单向关联
@Entity public class Tag { @Id private long id; private String tagName; }
@ManyToMany private List<Tag> tags;
生成的SQL如下,会多余的创建一个新的关系表。
CREATE TABLE `tag` ( `id` bigint(20) NOT NULL, `tag_name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `user_tags` ( `user_id` bigint(20) NOT NULL, `tags_id` bigint(20) NOT NULL, KEY `FKifh44dhy1ovc8a02b72ea5jd5` (`tags_id`), KEY `FKfcm4hc8oko2uqvf1bfypmp6st` (`user_id`), CONSTRAINT `FKfcm4hc8oko2uqvf1bfypmp6st` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`), CONSTRAINT `FKifh44dhy1ovc8a02b72ea5jd5` FOREIGN KEY (`tags_id`) REFERENCES `tag` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
双向关联,tag类修改如下:
@Entity public class Tag { @Id private long id; private String tagName; @ManyToMany(mappedBy = "tags") private List<User> users; }
此时生成的SQL语句和上文中的是一样的。接下来可以加上 @JoinTable来自定义关联列。User类修改如下:
@ManyToMany @JoinTable(name = "myUserTags",joinColumns = {@JoinColumn(name = "uid")},inverseJoinColumns = {@JoinColumn(name = "tid")}) private List<Tag> tags;
CREATE TABLE `my_user_tags` ( `uid` bigint(20) NOT NULL, `tid` bigint(20) NOT NULL, KEY `FKjq6w1do0t0ybthl9i65pg8h84` (`tid`), KEY `FKgceuixfh1q2eyjdkwyqxd1lr5` (`uid`), CONSTRAINT `FKgceuixfh1q2eyjdkwyqxd1lr5` FOREIGN KEY (`uid`) REFERENCES `user` (`id`), CONSTRAINT `FKjq6w1do0t0ybthl9i65pg8h84` FOREIGN KEY (`tid`) REFERENCES `tag` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@Repository
查看定义如下:
public interface Repository<T, ID extends Serializable> { }
@JpaRepository
他继承了PagingAndSortingRepository,PagingAndSortingRepository又继承自CrudRepository
@NoRepositoryBean public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> { /** * Returns all entities sorted by the given options. * * @param sort * @return all entities sorted by the given options */ Iterable<T> findAll(Sort sort); /** * Returns a {@link Page} of entities meeting the paging restriction provided in the {@code Pageable} object. * * @param pageable * @return a page of entities */ Page<T> findAll(Pageable pageable); }
@NoRepositoryBean public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> { /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll() */ List<T> findAll(); /* * (non-Javadoc) * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort) */ List<T> findAll(Sort sort); /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable) */ List<T> findAll(Iterable<ID> ids); /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) */ <S extends T> List<S> save(Iterable<S> entities); /** * Flushes all pending changes to the database. */ void flush(); /** * Saves an entity and flushes changes instantly. * * @param entity * @return the saved entity */ <S extends T> S saveAndFlush(S entity); /** * Deletes the given entities in a batch which means it will create a single {@link Query}. Assume that we will clear * the {@link javax.persistence.EntityManager} after the call. * * @param entities */ void deleteInBatch(Iterable<T> entities); /** * Deletes all entities in a batch call. */ void deleteAllInBatch(); /** * Returns a reference to the entity with the given identifier. * * @param id must not be {@literal null}. * @return a reference to the entity with the given identifier. * @see EntityManager#getReference(Class, Object) */ T getOne(ID id); /* (non-Javadoc) * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example) */ @Override <S extends T> List<S> findAll(Example<S> example); /* (non-Javadoc) * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) */ @Override <S extends T> List<S> findAll(Example<S> example, Sort sort); }
@CrudRepository
查看定义如下,提供了基础的CRUD方法
@NoRepositoryBean public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> { /** * Saves a given entity. Use the returned instance for further operations as the save operation might have changed the * entity instance completely. * * @param entity * @return the saved entity */ <S extends T> S save(S entity); /** * Saves all given entities. * * @param entities * @return the saved entities * @throws IllegalArgumentException in case the given entity is {@literal null}. */ <S extends T> Iterable<S> save(Iterable<S> entities); /** * Retrieves an entity by its id. * * @param id must not be {@literal null}. * @return the entity with the given id or {@literal null} if none found * @throws IllegalArgumentException if {@code id} is {@literal null} */ T findOne(ID id); /** * Returns whether an entity with the given id exists. * * @param id must not be {@literal null}. * @return true if an entity with the given id exists, {@literal false} otherwise * @throws IllegalArgumentException if {@code id} is {@literal null} */ boolean exists(ID id); /** * Returns all instances of the type. * * @return all entities */ Iterable<T> findAll(); /** * Returns all instances of the type with the given IDs. * * @param ids * @return */ Iterable<T> findAll(Iterable<ID> ids); /** * Returns the number of entities available. * * @return the number of entities */ long count(); /** * Deletes the entity with the given id. * * @param id must not be {@literal null}. * @throws IllegalArgumentException in case the given {@code id} is {@literal null} */ void delete(ID id); /** * Deletes a given entity. * * @param entity * @throws IllegalArgumentException in case the given entity is {@literal null}. */ void delete(T entity); /** * Deletes the given entities. * * @param entities * @throws IllegalArgumentException in case the given {@link Iterable} is {@literal null}. */ void delete(Iterable<? extends T> entities); /** * Deletes all entities managed by the repository. */ void deleteAll(); }
代码重构
通过对JPA注解的学习,我突然发现在上一篇的讲解中对好友关系的设计以及群组关系的设计不够优雅,导致我用了笨的方法实现。重新整理后关系如下:
用户 @OneToMany 好友分组(注:一个用户可以创建多个分组,每个人组只属于某个用户)
用户 @OneToMany 大群 (注:一个用户可以创建多个群,每个群只属于一个创建者)
用户 @ManyToMany 好友分组(注:每个分组内可以有多个好友,每个用户可以属于不同好友的分组内)
用户 @ManyToMany 大群(注:每个群可以有多个用户成员,每个用户可以属于不同的群)
代码如下:
@Entity public class User extends DomainBase{ private String avatar; private String userName; private String sign; //一对多,一个用户可以创建多个好友分组 @OneToMany(mappedBy = "owner") private List<FriendGroup> friendGroupsOwner; //多对多,因为一个分组可以有多个用户(好友) @ManyToMany @JoinTable(name = "user_friend_group",joinColumns = {@JoinColumn(name = "uid")},inverseJoinColumns = {@JoinColumn(name = "group_id")}) private List<FriendGroup> friendGroupsIn; @OneToMany(mappedBy = "owner") private List<BigGroup> bigGroupsOwner; @ManyToMany @JoinTable(name = "user_big_group",joinColumns = {@JoinColumn(name = "uid")},inverseJoinColumns = {@JoinColumn(name = "group_id")}) private List<BigGroup> bigGroupsIn; }
@Entity public class FriendGroup extends DomainBase { @Column(length = 20) private String name; @ManyToMany(mappedBy = "friendGroupsIn") private List<User> users; @ManyToOne @JoinColumn(name = "uid") private User owner; }
@Entity public class BigGroup extends DomainBase { @Column(length = 20) private String groupName; @Column(length = 120) private String avatar; @Column(length = 100) private String description; @ManyToMany(mappedBy = "bigGroupsIn") private List<User> users; @ManyToOne @JoinColumn(name = "uid") private User owner; }
重构后的代码清爽多了,关系也显而易见,去除了多余的类。service层的代码也更简洁。
/** * 获取init接口所需要的数据结果 *@param userId 用户ID *@return 返回 JsonResult(LayimInitDataViewModel) */ public JsonResult getBaseList(long userId){ LayimInitDataViewModel resultViewModel = new LayimInitDataViewModel(); //开始构造 //获取用户基本信息 User user = userRepository.findOne(userId); if(user == null){ return ResultUtil.fail(LAYIM_ENUM.NO_USER); } //映射用户信息 UserViewModel mine = LayimMapper.INSTANCE.mapUser(user); resultViewModel.setMine(mine); //获取好友分组信息 List<FriendGroup> friendGroups = user.getFriendGroups(); List<FriendGroupViewModel> friendGroupViewModels = new ArrayList<FriendGroupViewModel>(friendGroups.size()); //遍历好友分组 for (FriendGroup friendGroup : friendGroups){ List<User> usersInGroup = friendGroup.getUsers(); //先映射群组信息 FriendGroupViewModel friendGroupViewModel = LayimMapper.INSTANCE.mapFriendGroup(friendGroup); //将每个组的人放到好友分组里面 friendGroupViewModel.setList(LayimMapper.INSTANCE.mapUser(usersInGroup)); friendGroupViewModels.add(friendGroupViewModel); } resultViewModel.setFriend(friendGroupViewModels); //获取群组信息 List<BigGroup> bigGroups = user.getBigGroups(); resultViewModel.setGroup(LayimMapper.INSTANCE.mapBigGroup(bigGroups)); return ResultUtil.success(resultViewModel); }
运行程序,结果正常,偶也!
总结
对于JPA就介绍这么多吧,内容很浅。Repository里面还有很多有意思的东东,当然其他博客也有介绍的很多,并且比我写的详细,所以我就不在过多阐述了。不过后期都会用到,比如聊天记录保存,分页读取等。增删该查肯定是少不了的。
还有就是在学习的过程中也会对代码理解有更深的层次,所以不要只看,一定要自己敲出来。
下篇预告:从零一起学Spring Boot之LayIM项目长成记(五)websocket