1. 程式人生 > 實用技巧 >關於 Hibernate StackOverflowError 異常的一個解決方法

關於 Hibernate StackOverflowError 異常的一個解決方法

關於 Hibernate StackOverflowError 異常的一個解決方法

踩坑經歷

今天在學習 Hibernate 一對多對映的時候,使用 Hibernate 的建表策略進行建立資料庫表。執行測試類自動建立表並新增資料時,出現了個 bug 。報了 StackOverflowError 異常。報錯如下:

下面這個報錯是解決後重新模擬的,一開始報錯指向的是實體類的 toString 方法,這裡是指向 hashCode 方法。但是他們報錯的源頭都是相同的。

環境

我的實體類也很簡單,一個 User 類,一個 Car 類。他們之間的對應關係是一對多。一個使用者可以有很多輛小轎車。

先說下我的環境,拋開環境談異常都是耍流氓【狗頭】。但如果不想看環境可以直接跳到結果處

實體類使用了Lombok外掛,@Data 是自動給加了註解的類生成setter/getter、equals、canEqual、hashCode、toString方法。@AllArgsConstructor 是自動生成全參構造器。@NoArgsConstructor 是自動生成無參構造器

User

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    /**
     * id
     */
    private Integer id;
    /**
     * 使用者名稱
     */
    private String name;
    /**
     * 密碼
     */
    private String password;
    /**
     * 使用者擁有的車輛
     */

    private Set<Car> cars;
}

Car

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Car {
    /**
     * 車輛id
     */
    private Integer id;
    /**
     * 車輛名
     */
    private String name;
    /**
     * 車輛顏色
     */
    private String color;
}

Hibernate 核心配置如下:

hibernate.cfg.xml

<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <!-- 四個必填項 -->
        <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/hibernate?useTimezone=true&amp;serverTimezone=GMT%2b8&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;useSSL=false</property>
        <property name="hibernate.connection.username">root</property>
        <property name="hibernate.connection.password">root</property>

        <!-- 可選項 -->
        <!-- 顯示 SQL 語句 -->
        <property name="hibernate.show_sql">true</property>
        <!-- 顯示的 SQL 語句是否格式化,也就是說打印出來的 SQL 語句是一行還是多行顯示 -->
        <property name="hibernate.format_sql">true</property>

        <!-- 配置方言 -->
        <property name="hibernate.dialect">org.hibernate.dialect.MySQL57Dialect</property>

        <!-- 配置建表策略 -->
        <property name="hibernate.hbm2ddl.auto">update</property>

        <!-- 配置 mapping -->
        <mapping resource="mapper/User.hbm.xml"/>
        <mapping resource="mapper/Car.hbm.xml"/>
    </session-factory>
</hibernate-configuration>

Hibernate mapping配置如下:

User.hbm.xml

<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.xp.model.entity">
    <!-- orm 對映 實體類和資料表進行對映 -->
    <class name="User" table="user">
        <!-- 對映主鍵,其中name表示實體類中的成員變數,column 表示對應表中的欄位 -->
        <id name="id" column="id">
            <!-- 設定主鍵策略 -->
            <generator class="native"/>
        </id>
        <!-- 對映欄位,其中name表示實體類中的成員變數,column 表示對應表中的欄位 -->
        <property name="name" column="name"/>
        <property name="password" column="password"/>
        <!-- 一對多關係對映 -->
        <set name="cars" >
            <key column="cid"/>
            <one-to-many class="Car"/>
        </set>
    </class>
</hibernate-mapping>

Car.hbm.xml

<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.xp.model.entity">
    <class name="Car" table="car">
        <!-- 主鍵對映 -->
        <id name="id" column="id">
            <!-- 設定主鍵對映規則 -->
            <generator class="native"/>
        </id>
        <!-- 其他欄位對映 -->
        <property name="name" column="name"/>
        <property name="color" column="color"/>
        <!-- 多對一關係對映 -->
        <many-to-one name="user" class="User" column="cid"/>
    </class>
</hibernate-mapping>

測試類

@Test
public void test() {
    // 獲取會話
    Session session = HibernateUtil.getSession();
    // 初始化使用者和汽車
    User user = new User(null, "李老闆", "888", null);
    Car car1 = new Car(null, "寶馬", "紅色", null);
    Car car2 = new Car(null, "賓士", "白色", null);
    Car car3 = new Car(null, "保時捷", "藍色", null);

    // 讓使用者擁有三輛車
    Set<Car> cars = new HashSet<>();
    cars.add(car1);
    cars.add(car2);
    cars.add(car3);
    user.setCars(cars);
    // 讓車擁有主人
    car1.setUser(user);
    car2.setUser(user);
    car3.setUser(user);

    // 開啟事務,並將物件中儲存的資料寫入資料庫中
    Transaction transaction = session.beginTransaction();
    session.save(user);
    session.save(car1);
    session.save(car2);
    session.save(car3);
    transaction.commit();
    session.close();
}

一開始,我以為是 Lombok 的問題(也可以說是Lombok的問題,畢竟這個偷懶導致自己實體類的方法看不到),將 lombok 的註解註釋掉然後手動按照常規實體類編寫 getter,setter,構造器,toString() ,結果還是報錯。一開始用 lombok 時報錯時 hashcode 方法,現在報 toString 方法,然後我嘗試將 toString 方法去掉,然後就解決了。但是,報錯是解決了,但是整個人還是懵的。查了百度,發現其實他們講的並不是很全:刪除掉 toString 中關聯關係的物件。然後結合之前的 hashCode 方法的報錯以及報錯圖(在本文最上面)中重複刪除建立表,我推測是這樣的:

報錯原因及解決方法

主要原因就是父子表出現了雙向屬性,會導致 Hibernate 呼叫方法自動建立表或插入資料的時候死迴圈,方法不斷入棧,最終導致棧記憶體溢位

回看剛剛的程式碼,Lombok 幫我完成了 toString() 和 hashCode() 方法的重寫 ,所以這兩個方法裡面的具體演算法我是不知道的。而我自己手動寫實體類中的方法時,是用 idea 自動生成的(這年頭也沒多少人是完全手動寫的吧[狗頭]),預設toString 是輸出所有的成員變數。

問題就出在這

我們在方法中使用了關聯關係的物件。

所以解決方法就是:將實體類中事用了關聯關係的物件的方法進行修改,不單單是 toString() 方法。比如 hashCode 方法等