【问题标题】:I'm trying to understand LazyInitializationException and @Transactional我试图理解 LazyInitializationException 和 @Transactional
【发布时间】:2021-06-22 06:19:31
【问题描述】:

这是我之前的问题How to model packages, versions and licenses? 的后续问题。

这是我的数据库设置。

V1__create_table_license.sql

CREATE TABLE IF NOT EXISTS license (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    reference TEXT NOT NULL,
    is_deprecated_license_id BOOLEAN NOT NULL,
    reference_number INTEGER NOT NULL,
    license_id TEXT NOT NULL,
    is_osi_approved BOOLEAN NOT NULL
);

INSERT INTO license
  ("name",reference,is_deprecated_license_id,reference_number,license_id,is_osi_approved)
VALUES
  ('MIT License','./MIT.json',false,275,'MIT',true);

V2__create_npm_package.sql

CREATE TABLE IF NOT EXISTS npm_package (
    id BIGSERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    description TEXT NOT NULL
);

INSERT INTO npm_package 
    (name, description)
VALUES
    ('react', 'React is a JavaScript library for building user interfaces.'),
    ('react-router-dom', 'DOM bindings for React Router'),
    ('typescript', 'TypeScript is a language for application scale JavaScript development'),
    ('react-dom', 'React package for working with the DOM.');

V3__create_npm_version.sql

CREATE TABLE IF NOT EXISTS npm_package_version (
    npm_package_id BIGINT NOT NULL REFERENCES npm_package,
    version TEXT NOT NULL,
    license_id INTEGER NOT NULL REFERENCES license,

    UNIQUE(npm_package_id, version)
)

这是我的 Java 对象。

License.java

@Entity
public class License {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;

  private String reference;

  private Boolean isDeprecatedLicenseId;

  private Integer referenceNumber;

  private String name;

  private String licenseId;

  private Boolean isOsiApproved;
}

LicenseRepository.java

public interface LicenseRepository extends JpaRepository<License, Integer> {
  License findByLicenseIdIgnoreCase(String licenseId);
}

NpmPackage.java

@Entity
public class NpmPackage {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  private String description;

  @OneToMany(mappedBy = "npmPackage", cascade = CascadeType.ALL, orphanRemoval = true)
  private List<NpmPackageVersion> versions = new ArrayList<>();

  public NpmPackage() {}

  public void addVersion(NpmPackageVersion version) {
    this.versions.add(version);
    version.setNpmPackage(this);
  }

  public void removeVersion(NpmPackageVersion version) {
    this.versions.remove(version);
    version.setNpmPackage(null);
  }
}
@Entity
public class NpmPackageVersion {

  public NpmPackageVersion() {}

  public NpmPackageVersion(String version, License license) {
    this.setVersion(version);
    this.license = license;
  }

  @EmbeddedId private NpmPackageIdVersion npmPackageIdVersion = new NpmPackageIdVersion();

  @MapsId("npmPackageId")
  @ManyToOne(fetch = FetchType.LAZY)
  private NpmPackage npmPackage;

  @ManyToOne(fetch = FetchType.LAZY)
  private License license;

  @Embeddable
  public static class NpmPackageIdVersion implements Serializable {
    private static final long serialVersionUID = 3357194191099820556L;

    private Long npmPackageId;
    private String version;

    // ...
  }

  public String getVersion() {
    return this.npmPackageIdVersion.version;
  }

  public void setVersion(String version) {
    this.npmPackageIdVersion.version = version;
  }
}

MyRunner.java

@Component
class MyRunner implements CommandLineRunner {

  @Autowired LicenseRepository licenseRepository;

  @Autowired NpmPackageRepository npmPackageRepository;

  @Override
  // @Transactional
  public void run(String... args) throws Exception {
    // get license from database
    var license = licenseRepository.findByLicenseIdIgnoreCase("mit");

    // get package from db
    var dbPackage = npmPackageRepository.findByNameIgnoreCase("react");

    var version = new NpmPackageVersion("1.0.0", license);

    dbPackage.addVersion(version);
    
    npmPackageRepository.save(dbPackage);
  }
}

在我之前的问题中,我得到了使用fetch = FetchType.EAGER 的答案,但后来我了解到这并不理想。我想使用惰性获取。

@OneToMany(mappedBy = "npmPackage", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private List<NpmPackageVersion> versions = new ArrayList<>();

所以我删除了急切的获取并遇到了错误。

org.hibernate.LazyInitializationException:无法延迟初始化角色集合:com.example.bom.NpmPackage.NpmPackage.versions,无法初始化代理 - 没有会话

使用@Transactional 注释一切正常。为什么会这样?我试图在网上阅读所有内容,但我仍然不明白。我知道数据库会话在某个时候关闭,我想知道究竟是什么情况。我也想知道我是否可以做一些事情,例如我尝试获取所有版本以确保在添加另一个版本之前加载它们。

那么我真的必须使用@Transactional 还是有其他解决方案?我只是想了解正在发生的“魔术”:)

非常感谢!

【问题讨论】:

    标签: java postgresql spring-boot hibernate


    【解决方案1】:

    当您使用FetchType.LAZY 时,当您找到实体时,Hibernate ORM 并不会真正返回已初始化的集合。该关联将成为一个代理,当您需要访问该集合时,Hibernate ORM 将查询数据库并获取它。

    要实现这一点,实体(NpmPackage)需要处于托管状态。如果实体不受管理并且您尝试访问惰性关联(在这种情况下为versions),您将获得LazyInitializationException

    在您的示例中,当您使用 @Transactional 时,实体在方法期间保持受管理状态。没有它,一旦您从findByNameIgnoreCase 返回,它就会变得无法管理。

    如果您知道您将需要关联 versions,您还可以使用 fetch join 查询来获取 NpmPackage

    from NpmPackage p left join fetch p.versions where p.name=:name
    

    这样关联会保持惰性,但您可以通过单个查询获得它。

    【讨论】:

    • 谢谢。我想这也是我在别处读到的。我想知道EAGER 与您的fetch join 有何不同?
    • 是一样的。但是如果你在关联上声明它,关联将永远是 EAGER(不管你如何获得实体)。
    • 如果您使用查询进行此操作,则只有在对您的应用有意义时才能急切地检索关联
    猜你喜欢
    • 2022-11-30
    • 1970-01-01
    • 2017-02-23
    • 2019-07-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-03
    • 1970-01-01
    相关资源
    最近更新 更多