【问题标题】:How to generate a value for a column in a JPA entity, while querying the database?如何在查询数据库时为 JPA 实体中的列生成值?
【发布时间】:2015-11-20 20:59:22
【问题描述】:

我有一个看起来像这样的实体:

@Entity
@Table(uniqueConstraints={@UniqueConstraint(columnNames={"slug"})})
public class BlogPost {

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

    @Column
    private String title;

    @Column
    private String slug;    
}

我想通过执行以下操作在持久化之前生成 slug 的值:

  • title 从例如博客文章标题博客文章标题
  • 确保 blog-post-title 在表 BlogPost 中是唯一的,如果它不是唯一的,我想在标题上附加一些后缀,使其变为例如blog-post-title-2

因为我在很多实体上都需要这个,所以我最初的想法是创建一个 EntityListener,它会在 @PrePersist 上执行此操作。但是,文档通常声明我不应该调用 EntityManager 或 Query 方法,也不应该从生命周期回调中访问任何其他实体对象。我需要这样做以确保我生成的 slug 确实是独一无二的。

我试图变得厚颜无耻,但无论如何使用 Spring 将存储库自动装配到 EntityListener 确实非常困难。

我应该如何最好地解决这个问题?

谢谢!

【问题讨论】:

    标签: spring hibernate jpa


    【解决方案1】:

    OndrejM 和 MirMasej 都绝对正确,生成 slug 不会在实体中完成。我希望 EntityListeners 可以“更聪明”一点,但这不是一个选择。

    我最终做的是使用 aspects 来完成我想要的。我不是“挂钩”实体,而是挂钩 CrudRepository 的保存方法。

    首先,我创建了一个注解,这样我就可以识别出哪个字段需要被 sluggified:

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface Slug {
    
        /**
         * The string slug is generated from 
         */
        String source() default "title";
    
        /**
         * Strategy for generating a slug
         */
        Class strategy() default DefaultSlugGenerationStrategy.class;
    }
    

    然后,我创建了一个类似这样的方面:

    @Aspect
    @Component
    public class SlugAspect {
    
        ... // Removed some code for bravity
    
        @Before("execution(* org.springframework.data.repository.CrudRepository+.save(*))")
        public void onRepoSave(JoinPoint joinPoint) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Object entity = joinPoint.getArgs()[0];
    
            for (Field field: entity.getClass().getDeclaredFields()) {
                Slug annotation = field.getAnnotation(Slug.class);
    
                if (annotation != null) {
                    CrudRepository repository = (CrudRepository) joinPoint.getTarget();
    
                    Long count = 0L;
                    SlugGenerationStrategy generator = (SlugGenerationStrategy)annotation.strategy().newInstance();
                    String slug = generator.generateSlug(slugOrigin(entity));
    
                    if (id(entity) != null) {
                        Method method = repository.getClass().getMethod("countBySlugAndIdNot", String.class, Long.class);
                        count = (Long)method.invoke(repository, slug, id(entity));
                    } else {
                        Method method = repository.getClass().getMethod("countBySlug", String.class);
                        count = (Long)method.invoke(repository, slug);
                    }
    
                    // If count is zero, use the generated slug, or generate an incremented slug if count > 0 and then set it like so:
                    setSlug(entity, slug);
                }
            }
        }
    }
    

    如果有人对以下内容感兴趣,我会将代码放在 github 上(尽管它仍然只是一个概念证明):https://github.com/cabrilo/jpa-slug

    它依赖于来自 Spring Data 的 CrudRepository 并在 repo 上拥有这两个方法:countBySlug 和 countBySlugAndIdNot。

    再次感谢您的回答。

    【讨论】:

    • 如果此解决方案有效并且您想在许多实体中生成字段,那么这可能是要走的路。虽然对我来说有点过于复杂和难以接受,因为当您查看实体时,您不知道 slug 的用途以及修改的位置。更具可读性的是为每个具有此行为的实体在方法 setTitle 周围放置一个方面或拦截器。
    • @Dejan 太好了,您提供的解决方案令您满意,而且还可以重复使用。请注意缺点。想象一下,您对实体进行了一些更改,例如其中一个具有数据库不允许的状态,因此更新或插入将失败。现在,你用 slug 做你的事情,Aspect 在有查询的地方发挥作用,ORM 框架在查询之前执行刷新,你得到一个异常。找出问题所在并不容易。
    • @OndrejM 我同意将它放在 setTitle 上会很有意义,但会有一些缺点,主要是在生成 slug 时我至少需要一个数据库查询 - 并且逻辑可能会变得更复杂(例如,在 slug 更改后如何处理重定向)。所以我的想法是在最后可能的时候这样做(这就是我最初想使用 EventListener 和 PrePersist 回调的原因)。
    • @MirMasej - 这是我没有考虑过的情况。感谢您的提醒!
    【解决方案2】:

    最直接的解决方案似乎是在设置标题的值之前进行检查。然而,这意味着计算 slug 的逻辑将在实体之外,并且两者都来自外部。

    【讨论】:

      【解决方案3】:

      您必须将实体视为与数据库没有任何连接的普通对象——这就是 ORM 的理念。但是,您可以将 EntityManager 或 DAO 的引用作为附加参数传递给 setter 方法,或者以某种方式注入对它的引用。然后,您可以直接从 setter 方法调用查询。此解决方案的缺点是您需要始终提供 EntityManager,无论是在设置标题时,还是在创建/加载实体时。 这是解决这个问题的最好的面向对象方式。

      【讨论】:

        猜你喜欢
        • 2020-04-02
        • 2016-01-02
        • 2011-08-15
        • 1970-01-01
        • 1970-01-01
        • 2014-12-26
        • 1970-01-01
        • 2011-10-14
        • 1970-01-01
        相关资源
        最近更新 更多