在用jpa這種orm框架時,有時我們實體對象存在關聯關系,但實際的業務場景可能不需要用jpa來控制數據庫創建數據表之間的關聯約束,這時我們就需要消除掉數據庫表與表之間的外鍵關聯。
但jpa在處理建立外鍵時存在一些問題,在stackoverflow上搜索了相關的jpa創建實體對象關聯關系但不建立外鍵這一系列問題后,發現這個是jpa在處理外鍵時存在一定的bug,官方給出的答復是在hibernate 5.x會解決掉這個問題,但是經驗證5.x的版本這個問題依舊存在。下面給出這個問題的解釋以及這個問題如何解決。
下面會以techer和student對象來舉例,teacher和student存在一對多關系,一個teacher關聯多個student。
1.teacher與student設置外鍵關系
teacher和student之間通過@OneToMany和@ManyToOne建立外鍵關聯關系
teacher:
@Entity @Table(name = "TEACHER") public class Teacher extends BaseDomain { @Id() @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID") private Long id; @Column private String name; @OneToMany(mappedBy = "teacher") private List<Student> students; //getter&setter... }
student:
@Entity @Table(name = "STUDENT") public class Student extends BaseDomain { @Id() @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID") private Long id; @Column private String name; @ManyToOne @JoinColumn(name = "tid") private Teacher teacher; //getter&setter... }
數據庫生成表結果:
CREATE TABLE “public”.”student” ( “id” int8 DEFAULT nextval(‘student_id_seq’::regclass) NOT NULL, “name” varchar(255) COLLATE “default”, “teacher_id” int8, CONSTRAINT “student_pkey” PRIMARY KEY (“id”), CONSTRAINT “fk3y5qg5r9ewc48x7ek8lx5ua8h” FOREIGN KEY (“teacher_id”) REFERENCES “public”.”teacher” > (“id”) ON DELETE NO ACTION ON UPDATE NO ACTION ) WITH (OIDS=FALSE) ; ALTER TABLE “public”.”student” OWNER TO “postgres”;
可以看到設置了外鍵”fk3y5qg5r9ewc48x7ek8lx5ua8h” FOREIGN KEY (“teacher_id”)
2.只在student端加上@ForeignKey
student
@Entity @Table(name = "STUDENT") public class Student extends BaseDomain { @Id() @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID") private Long id; @Column private String name; @ManyToOne @JoinColumn(name = "tid",foreignKey = @ForeignKey(name = "none",value = ConstraintMode.NO_CONSTRAINT)) private Teacher teacher; //setter&getter }
加上該注解之后,根據這個注解說明是可以去掉外鍵關聯關系,但發現加上后然並卵,外鍵還是沒有去掉。這里需要說明其中@ForeignKey的value值由如下代碼所示幾種情況:
/** * Used to control the application of a constraint. * * @since JPA 2.1 */ public enum ConstraintMode { /** * Apply the constraint. */ CONSTRAINT, /** * Do not apply the constraint. */ NO_CONSTRAINT, /** * Use the provider-defined default behavior. */ PROVIDER_DEFAULT }
3.在teacher端加入@org.hibernate.annotations.ForeignKey(name = “none”)
在一的這端加上@org.hibernate.annotations.ForeignKey(name = “none”)這個被jpa廢棄的注解。加上之前在student中設置的@ForeignKey(注意這個是javax.persistence包下的),可以去掉外鍵關聯
teacher:
@Entity @Table(name = "TEACHER") public class Teacher extends BaseDomain { @Id() @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID") private Long id; @Column private String name; @OneToMany(mappedBy = "teacher") @org.hibernate.annotations.ForeignKey(name = "none") private List<Student> students; //getter&setter... }
結果:
CREATE TABLE “public”.”student” ( “id” int8 DEFAULT nextval(‘student_id_seq’::regclass) NOT NULL, “createdtime” timestamp(6), “name” varchar(255) COLLATE “default”, “version” int4, “tid” int8, CONSTRAINT “student_pkey” PRIMARY KEY (“id”) ) WITH (OIDS=FALSE) ; ALTER TABLE “public”.”student” OWNER TO “postgres”;
可以看到student表中原來關聯teacher的外鍵沒了,說明該注解起作用。
4.需要注意的坑
1.如果teacher(1的這端)有student列表(多的這端),像這樣:
@OneToMany(mappedBy = "teacher") @org.hibernate.annotations.ForeignKey(name = "none") private List<Student> students;
如果要去掉外鍵關聯關系,student端也需要像在2小結提到樣需要加上@JoinColumn(name = “tid”,foreignKey = @ForeignKey(name = “none”,value = ConstraintMode.NO_CONSTRAINT)),但此時你會發現其中@ForeignKey中value的值不管你設置為ConstraintMode.NO_CONSTRAINT還是ConstraintMode.CONSTRAINT,數據庫都不會設置外鍵(這個是特么的真奇怪)。但是就是不管怎樣,你就是不能不設置@ForeignKey,並且你還必須要設置其中的值不能為默認值,不然就是要生成外鍵。這里貼下@ForeignKey的源碼:
@Target({}) @Retention(RUNTIME) public @interface ForeignKey { /** * (Optional) The name of the foreign key constraint. Defaults to a provider-generated name. * * @return The foreign key name */ String name() default ""; /** * (Optional) The foreign key constraint definition. Default is provider defined. If the value of * disableForeignKey is true, the provider must not generate a foreign key constraint. * * @return The foreign key definition */ String foreignKeyDefinition() default ""; /** * (Optional) Used to specify whether a foreign key constraint should be generated when schema generation is in effect. */ ConstraintMode value() default ConstraintMode.CONSTRAINT; }
真的是X了狗了。。。我表示久久不能理解。。。
2.teacher(1這端)沒有student列表或者student列表被@Transient所修飾,像這樣:
@OneToMany(mappedBy = "teacher") @Transient private List<Student> students;
那么也是無論你在student端設置ConstraintMode的值,都不會設置外鍵.but!!!你就是不能不在student端(多的這端)設置@JoinColumn(name=”tid”,foreignKey=@ForeignKey(name=”none”,value=ConstraintMode.NO_CONSTRAINT)),否則也是會生成外鍵
總結
所以要使數據表中沒有外鍵關聯關系。
1.當兩邊都有關聯關系字段,1的這端利用@org.hibernate.annotations.ForeignKey(name = “none”),多的那端在JoinColumn中加上foreignKey = @ForeignKey(name = “none”,value = ConstraintMode.NO_CONSTRAINT)
2.當只有多的那端有關聯字段,一的那段沒有關聯字段或者關聯字段被@Transient所修飾,請在多的那端在JoinColumn中加上foreignKey = @ForeignKey(name = “none”,value = ConstraintMode.NO_CONSTRAINT)
最后需要說明的是@org.hibernate.annotations.ForeignKey(name = “none”)這個注解之后可能會在之后的版本會被直接移除掉,所以更新jar包的時候需要注意下。
參考資料:
①.https://hibernate.atlassian.net/browse/HHH-8805
②.https://hibernate.atlassian.net/browse/HHH-8862
親測可用;
第二種辦法:
關於如何禁用Hibernate生成外鍵,網上有使用設置ForeignKey(name="null")方式,使Hibernate不生成外鍵關聯,但是需要在每個關聯關系上設置,比較繁瑣,很難統一控制保證數據庫中不存在外鍵關聯。而且經測試在@JoinColumn設置foreignkey=@ForeignKey(name="null")不會生成外鍵,在@JoinTable中此種設置方式還是可以生成外鍵。
下面提供一種禁用Hibernate外鍵的方式,在創建數據庫表時不生成外鍵關聯,但是個人感覺還不是最好的解決方案,希望多多指教。
思路:因為Hibernate為了處理不同數據庫SQL的差異,為每個數據庫定義了dialect,在執行SQL時會由dialect類的方法中獲取相應的SQL,所以可以通過重寫dialect類中生成外鍵SQL的方法不生成數據庫外鍵關聯。分別重寫的Postgresql數據庫和Oracle數據庫的Dialect類如下:
import org.hibernate.dialect.PostgreSQL9Dialect; /** * 不生成外鍵,通過類似於SQL注入的方法,為每個數據庫修改創建外鍵的SQL */ public class PostgreSQL9DialectWithoutFK extends PostgreSQL9Dialect { @Override public String getAddForeignKeyConstraintString( String constraintName, String[] foreignKey, String referencedTable, String[] primaryKey, boolean referencesPrimaryKey) { // 設置foreignkey對應的列值可以為空 return " alter "+ foreignKey[0] +" set default null " ; } }
import org.hibernate.dialect.Oracle10gDialect; /** * 不生成外鍵,通過類似於SQL注入的方法,為每個數據庫修改創建外鍵的SQL */ public class Oracle10gDialectWithoutFK extends Oracle10gDialect { @Override public String getAddForeignKeyConstraintString( String constraintName, String[] foreignKey, String referencedTable, String[] primaryKey, boolean referencesPrimaryKey) { // 通過修改外鍵列的默認值,而不是添加外鍵,避免生成外鍵 return " modify "+ foreignKey[0] +" default null " ; } }
在重寫生成外鍵SQL時考慮過使用SQL注入的方式在創建完外鍵后,再刪除外鍵,但是這種方式比較復雜,做了一點后就放棄了。
創建了重寫的Dialect類,通過hibernate.dialect=Oracle10gDialectWithoutFK配置后,在生成數據庫表時外鍵策略就會生效。
另:
- 關於是否在數據庫中生成外鍵的討論如下圖,具體的討論內容可以按關鍵字搜索相關內容。
- JPA中@JoinColumn不生成外鍵的配置
- postgresql中alter語法如下