当持久化的属性并不是基本数据类型,也不是字符串,日期等变量,而是一个复杂类型的对象,这个对象就称为组件属性。在持久化过程中,它仅仅被当做值类型,而并非引用另一个持久化类实体。组件属性的类型可以是任意的自定义类。

@Entity
@Table(name="persona_inf")
public class PersonA {

@Id @Column(name="person_id")
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;

private int age;
private Name name;

}
@Embeddable
public class Name {
@Column(name="person_firstname")
private String first;
@Column(name="person_lastname")
private String last;
@Parent
private PersonA owner;
public Name(){}
public Name(String first, String last){
	this.first=first;
	this.last=last;
}
}
private static void createAndStorePersons() {
		Session session = HibernateUtil.getSession();

		Transaction tx = session.beginTransaction();
		
		PersonA p=new PersonA();
		p.setAge(99);
	
		p.setName(new Name("Alvin","Meng"));
		session.save(p);
		
		tx.commit();
		session.close();

	}

mysql> select * from persona_inf;

+-----------+-----+------------------+-----------------+

| person_id | age | person_firstname | person_lastname |

+-----------+-----+------------------+-----------------+

|         1 |  99 | Alvin            | Meng            |

+-----------+-----+------------------+-----------------+

1 row in set (0.00 sec)


我们注意到Person的name属性不是基本类型或者String,而是自定义类。不但无法指定列明,也无需任何标注。为了让PersonA与Name建立起联系,我们在Name中使用@Embaddable来指定这是一个组件。然后标注两列。再使用@Parent指明谁是它的拥有者。最后还要提供两个构造器。为什么需要构造器呢?因为在给name设值的时候并没有显式地new一个实例变量,因此需要使用构造器自动创建。从数据库中的表结构我们可以看出,Hibernate会把Name的每一个属性映射称为一个数据列。

Hibernate还提供了另一种组件映射策略。这种策略无需再组件雷尚使用@Embeddable注解,而只需要在持久化类中使用@Embedded注解修饰组件属性。然后再使用@AttributeOverrides来指明组件属性的属性值。每个属性值再用@AttributeOverride标注,可以有name,column等。

@Entity
@Table(name = "personb_inf")
public class PersonB {
	@Id
	@Column(name = "person_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	private int age;

	@Embedded
	@AttributeOverrides({
			@AttributeOverride(name = "first", column = @Column(name = "person_first")),
			@AttributeOverride(name = "last", column = @Column(name = "person_lastname"))

	})
	private Name name;
}

如果这样配置,那么Name类就是一个普通的bean类。没有Parent,也没有任何注解(表头还是需要@Embeddable),只有两个属性跟set,get方法,还有两个构造器。


组合属性为集合

如果组件类里面又包括了List、Set、Map等集合属性,则可以直接在组件中使用@ElementCollection修饰集合属性,并使用@CollectionTable指定保存集合属性的表明。在上面例子的基础上,我们再给Name属性增加一个Map类型的Power属性。

@Embeddable
public class Name {
@Column(name="first_name")
private String first;
@Column(name="last_name")
private String last;

@ElementCollection(targetClass=Integer.class)
@CollectionTable(name="power_inf")
@MapKeyColumn(name="name_aspect")
@Column(name="name_power",nullable=false)
@MapKeyClass(String.class)
private Map<String,Integer> power=new HashMap<>();


public Name(){}
public Name(String first, String last){
	this.first=first;
	this.last=last;
}
}
@Entity
@Table(name = "personc_inf")
public class PersonB {
	@Id
	@Column(name = "person_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	private int age;

	private Name name;
}
private static void createAndStorePersons() {
		Session session = HibernateUtil.getSession();

		Transaction tx = session.beginTransaction();
		
		PersonB p=new PersonB();
		p.setAge(99);
	
		Name n=new Name("meng","cao");
		n.getPower().put("math", 100);
		n.getPower().put("history", 100);

		n.getPower().put("art", 100);

		p.setName(n);
		
		session.save(p);
		
		tx.commit();
		session.close();

	}

如果Name类中没有显示地实例化一个Map对象,那么这里就要先实例化对象,然后设置,然后再设给Name的Map属性了。如此运行,数据库中就有了两张表。personc_inf里面记录了id号,firstname与lastname。而是用id作为外键的power表里面有了我们设置的三行数据。所以,Hibernate依然将Name属性映射成两列,而Name属性的Map属性则是是用额外的表来存储,并且建立主键外键关系。由此可见虽然Map是Name的属性,但从键上看它也是Person的属性。


集合属性的元素为组件

再放一波大招,集合元素除了可以保存上述的String跟int,还可以保存组件对象。实际上更多的情况下集合里面保存的都是组件对象。对于集合元素是组件的集合属性,我们仍然使用@ElementCollection标注这是一个集合,使用@CollectionTable指定使用哪个表来保存,使用@OrderColumn或者对于Map使用@MapKeyColumn指定映射索引。不同的是程序不再使用@Column映射保存集合元素的数据列,因为我们无法使用单独的数据列保存一个集合元素!跟上上面的例子一样, 我们只需要使用@Embeddable来指定一个组件类即可。

@Entity
@Table(name="persond_inf")
public class PersonD {

@Id @Column(name="person_id")
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;

private int age;

@ElementCollection(targetClass=Score.class)
@CollectionTable(name="scored_inf",joinColumns=@JoinColumn(name="person_id",nullable=false))
@MapKeyColumn(name="subject_name")
@MapKeyClass(String.class)
private Map<String,Score> scores=new HashMap<>();

@ElementCollection(targetClass=Name.class)
@CollectionTable(name="nick_inf",joinColumns=@JoinColumn(name="person_id",nullable=false))
@OrderColumn(name="list_order")
private List<Name> nicks=new ArrayList<>();

}
@Embeddable
public class Score {
@Column(name="score_level")
private String level;
@Column(name="score_number")
private int number;
public Score(){}
public Score(String level,int number){
	this.level=level;
	this.number=number;
}
}
@Embeddable
public class Name {
@Column(name="person_firstname")
private String first;
@Column(name="person_lastname")
private String last;
@Parent
private PersonD owner;
public Name(){}
public Name(String first,String last){
	this.first=first;
	this.last=last;
}
}

mysql> select * from persond_inf;

+-----------+-------+

| person_id | age   |

+-----------+-------+

|         1 | 10000 |

|         2 |     6 |

+-----------+-------+


mysql> select * from scored_inf;

+-----------+-------------+--------------+--------------+

| person_id | score_level | score_number | subject_name |

+-----------+-------------+--------------+--------------+

|         1 | C           |           74 | firstyear    |

|         1 | A           |           99 | secondyear   |

|         2 | B-          |           88 | firstyear    |

|         2 | F           |           49 | secondyear   |

+-----------+-------------+--------------+--------------+


mysql> select * from nick_inf;

+-------------+------------------------+------------------------+-------------+

| person_id | person_firstname | person_lastname | list_order |

+-------------+------------------------+------------------------+-------------+

|         1      | meng                    | cao                       |          0     |

|         2      | jing                       | shi                         |          0     |

|         2      | hui                        | yu                          |          1     |

+-------------+------------------------+-------------------------+------------+


如图所示,主表是persond_inf,里面有两条记录。里面有两列,第一列是自增长主键。其实我们可以想象它还有另外两列,其中一个保存了一个Map集合指定scored,另一个保存了一个List集合指定nick。对于每个personid,scored表是Map集合,里面的subject_name是Map的key,而value则是一个Score对象,它有level跟number两个属性。事实上我们也可以指定更多属性。没错,这里有用到了之前我们讲的知识,如果一个集合里面装的是基本类型,那么就用列来代表这些属性吧。nick表示一个List,里面装了Name这个类,用两列表示。而list_order则是有序集合List的索引,用来跟person_id组成联合主键。为啥Map里面没有索引组成联合主键呢?有的!Map中使用的不是OrderColumn,而是MapKeyColumn,使用的是key,而不是自增长的数字。因此map中是key与外键组成联合主键。


组件作为Map的索引

由于Map集合的特殊性,它允许使用一个符合类型的对象作为它的key。对于这种情况,依然使用@ElementCollection修饰集合属性,使用@CollectionTable指定保存表。由于索引是一个类,因此我们使用@MapKeyClass注解指定key的类型。

@Entity
@Table(name="persone_inf")
public class PersonE {
	@Id @Column(name="person_id")
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	private Integer id;
	private int age;
	
@ElementCollection(targetClass=Score.class)
@CollectionTable(name="nick_power_inf",joinColumns=@JoinColumn(name="person_id",nullable=false))	
@Column(name="nick_power",nullable=false)
@MapKeyClass(Name.class)
private Map<Name,Integer> nickPower=new HashMap<Name,Integer>();	

}
@Embeddable
public class Name {
	@Column(name = "person_firstname")
	private String first;
	@Column(name = "person_lastname")
	private String last;
	@Parent
	private PersonD owner;

	public Name() {}

	public Name(String first, String last) {
		this.first = first;
		this.last = last;
	}

	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj != null && obj.getClass() == Name.class) {
			Name target = (Name) obj;
			return target.getFirst().equals(getFirst())
					&& target.getLast().equals(getLast());
		}
		return false;
	}

	public int hashCode() {
		return getFirst().hashCode() * 31 + getLast().hashCode();

	}
}

持久化类使用Name对象作为Map的key,所以程序应该重写Name类的equals()和hashCode()两个方法。除此之外,程序还使用@Embeddable修饰组件类。


mysql> select * from persone_inf;

+-----------+-----+

| person_id | age |

+-----------+-----+

|         1 |   6 |

+-----------+-----+


mysql> select * from nick_power_inf;

+-----------+------------+-------+------+

| person_id | nick_power | first | last |

+-----------+------------+-------+------+

|         1 |         23 | meng  | cao  |

|         1 |         24 | jing  | shi  |

+-----------+------------+-------+------+


mysql> desc nick_power_inf;

+------------+--------------+------+-----+---------+-------+

| Field      | Type         | Null | Key | Default | Extra |

+------------+--------------+------+-----+---------+-------+

| person_id  | int(11)      | NO   | PRI | NULL    |       |

| nick_power | int(11)      | NO   |     | NULL    |       |

| first      | varchar(255) | NO   | PRI |         |       |

| last       | varchar(255) | NO   | PRI |         |       |

+------------+--------------+------+-----+---------+-------+


图中我们能够看到,主表中只有两列,一个是自动生成的主键,另一个是普通age属性。其实可以想象它还有一列,用于保存Map集合。Map集合中key是一个类,因为它有两个属性,所以是用两列来保存。它的value是nick_power。注意代码中的@ElementCollection里面的targetClass应该制定value的类型!集合里存什么,就是什么class!不要觉得list只有一列,Map有key跟value两列!有了索引之后都是两列了!!!md我写代码时候没注意报错debug了好久。通过description我们发现这里使用了外键,first跟last作为联合主键!


组件作为复合主键

简单的逻辑主键不涉及这个问题。当组件作为主键时,才涉及!作为标识符的组件必须有无参数的构造器,实现Serializable接口!建议重写equals和hashCode方法。Hibernate4中第二条不必要。组件作为主键,Hibernate无法为复核主键自动生成键值,所以程序必须为持久化实例分配这种组件标识符。

@Entity
@Table(name="persone_inf")
public class PersonE {
	@EmbeddedId 
	@AttributeOverrides({
		@AttributeOverride(name="first",column=@Column(name="person_firstname")),
		@AttributeOverride(name="last",column=@Column(name="person_lastnnnmae"))				
	})
	private int age;	
}
public class AName implements Serializable {
	private String first;
	private String last;
	public AName(){}
	public AName(String first,String last){
		this.first=first;
		this.last=last;
	}
	
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj != null && obj.getClass() == Name.class) {
			Name target = (Name) obj;
			return target.getFirst().equals(getFirst())
					&& target.getLast().equals(getLast());
		}
		return false;
	}
	public int hashCode() {
		return getFirst().hashCode() * 31 + getLast().hashCode();
	}

}
private static void createAndStorePersons() {
		Session session = HibernateUtil.getSession();
		Transaction tx = session.beginTransaction();
		
		PersonE p=new PersonE();
		p.setAge(6);
		p.setName(new AName("aa","bb"));
		
		session.save(p);		
		tx.commit();
		session.close();
	}

mysql> select * from persone_inf;

+------------------+-------------------+-----+

| person_firstname | person_lastnnnmae | age |

+------------------+-------------------+-----+

| aa               | bb                |   6 |

+------------------+-------------------+-----+


mysql> desc persone_inf;

+-------------------+--------------+------+-----+---------+-------+

| Field             | Type         | Null | Key | Default | Extra |

+-------------------+--------------+------+-----+---------+-------+

| person_firstname  | varchar(255) | NO   | PRI | NULL    |       |

| person_lastnnnmae | varchar(255) | NO   | PRI | NULL    |       |

| age               | int(11)      | NO   |     | NULL    |       |

+-------------------+--------------+------+-----+---------+-------+



数据表中可以看到主键变成了first跟lastname。代码中我们使用@EmbeddedId来标识这个AName类型的表示属性,然后配置列与底层数据库的映射关系。由于标识属性不能简单地自增,所以test类中我们必须手动地设置。


多列作为联合主键

Hibernate还提供了另一种联合主键支持,允许将持久化类的多个属性映射成联合主键。如果想要这样做,持久化类必须有无参数的构造器,实现Serializable接口,重写equals和hashCode方法。我们只需要简单地使用@Id标注我们想要成为主键的属性即可。跟刚才的差不多,只不过这里不再是一个组件的两个属性,而是持久化类的两个属性。