4

I am coding a ribbon/achievements system for a website and I have to write some logic for each ribbon in my system. For example, you could earn a ribbon if you're in the first 2,000 people registering to the website or after 1,000 post in the forum. The idea is very similar to stackoverflow's badges, really.

So, every ribbon is obviously in the database but they also need a bit of logic to determine when a user has earned the ribbon.

In the way I coded it, Ribbon is a simple abstract class:

@Entity
@Table(name = "ribbon")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "ribbon_type")
public abstract class Ribbon
{
    @Id
    @Column(name = "id", nullable = false, length = 8)
    private int id;

    @Column(name = "title", nullable = false, length = 64)
    private String title;

    public Ribbon()
    {
    }

    public abstract boolean isEarned(User user);

    // ... getters/setters...
}

You can see I define the inheritance strategy as SINGLE_TABLE (since I have to code like 50 ribbons and I don't need additional columns for any of them).

Now, a specific ribbon will be implemented like this, for example:

@Entity
public class First2000UsersRibbon extends Ribbon
{
    @Autowired
    @Transient
    private UserHasRibbonDao userHasRibbonDao;

    public First2000UsersRibbon()
    {
        super.setId(1);
        super.setTitle("Between the first 2,000 users who registered to the website");
    }

    @Override
    public boolean isEarned(User user)
    {
        if(!userHasRibbonDao.userHasRibbon(user, this))
        {
            // TODO
            // All the logic to determine whether the user earned the ribbon
            // i.e. check whether the user is between the first 2000 users who registered to the website
            // Other autowired DAOs are needed
        }
        else
        {
            return true;
        }

        return false;
    }
}

The problem is that userHasRibbonDao is null inside the isEarned() method, so a NullPointerException is thrown.

I thought that having DAOs autowired into domain objects was wrong, but in this topic they told me that it's the correct approach (Domain-Driven Design).

I shared a non-working very simple example on GitHub: https://github.com/MintTwist/TestApp (remember to change the connection details in /WEB-INF/properties/jdbc.properties and to import the test_app.sql script)

Any help very appreciated.

Thank you!

Update - Reading the first answers, it seems like my approach is completely wrong. How would you ideally structure the code given that there may be 50-70 different ribbons? Thanks

4

8 回答 8

6

我并不是说我同意将 DAO 注入到域实例中……但是

您可以将您的 DAO 连接到您的域对象中。但是你必须在你的 spring 应用程序上下文中声明你的域对象,并从 Spring 中获取新实例,而不是使用new. 确保您使用原型范围!您不想每次都获得相同的单例实例!

实际上,您想要实现的这个逻辑属于一个注入了它需要的 DAO 的服务。

也许你可以有这样的服务:

@Service
public class RibbonServiceImpl implements RibbonService

  @Autowired
  private RibbonDAO ribbonDAO;

  public boolean isEarned(Ribbon ribbon, User user) {
   if(!userHasRibbonDao.userHasRibbon(user, this))
        {
            // TODO
            // All the logic to determine whether the user earned the ribbon
            // i.e. check whether the user is between the first 2000 users who registered to the website
            // Other autowired DAOs are needed
        }
        else
        {
            return true;
        }

        return false;
  }  
于 2012-06-15T19:47:40.677 回答
4

将其标记为@Configurable- @Configurable注释将确保即使 bean 是在 Spring 之外创建的,也会注入依赖项

您还需要<context:spring-configured/>在您的上下文中添加。

于 2012-06-15T20:40:57.257 回答
2

缺少一个答案,它并不漂亮,但它有效。您可以从 WebApplicationContext 中查找它,而不是连接 Dao:

RibbonDao dao = ContextLoader.getCurrentWebApplicationContext.getBean(RibbonDao.class);

这与依赖注入所代表的一切相反(我喜欢将此模式称为“控制反转反转”:-)),但是:将服务注入域对象也是如此。

于 2012-06-24T11:02:10.283 回答
0

从我可以看到你的休眠类的设计,以及获得的丝带的持久性,都很好。我认为问题在于您何时以及如何确定用户是否获得了新丝带。

假设一个新请求来自登录用户。User 对象由 Hibernate 创建和填充,我们现在知道该用户已经在 userHasRibbonSet 中获得的所有 Ribbon。我们可能需要在 User 中使用这样的方法:

public boolean hasEarnedRibbon(Ribbon ribbon) {
    for (UserHasRibbon userHasRibbon : userHasRibbonSet) {
        if (userHasRibbon.getRibbon().equals(ribbon) {
            return true;
        }
    }
    return false;
}

(这可能可以通过将功能区本身缓存在 Set 中并进行恒定时间查找来优化,但这不是关键)

请求被处理,用户对象被更新以反映发生了什么。然后,在出去的路上,你检查用户现在获得了什么丝带,像这样:

public class RibbonAwardingInterceptor extends HandlerInterceptorAdapter {

    @Resource
    private SessionFactory sessionFactory;
    @Resource // assuming it's a request-scoped bean; you can inject it one way or another
    private User user;

    public void postHandle(HttpServletRequest request, HttpServletResponse response, 
       Object handler, ModelAndView modelAndView) throws Exception {

        List<Ribbon> allRibbons = sessionFactory.getCurrentSession().createQuery("from Ribbon").list();

        for (Ribbon ribbon : allRibbons()  {
            if (!user.hasEarnedRibbon(ribbon)) {
                // The user has not previously earned this ribbon - lets see if they have now
                if (ribbon.isEarned(user)) {
                    user.getUserHasRibbonSet().add(new UserHasRibbon(user, ribbon));
                }
            }
        }
    }
}

如果您想使用这种确切的模式,请确保此拦截器在任何以与功能区相关的方式更新用户的拦截器之后,但在关闭事务的拦截器之前(假设您使用的是每请求事务模型)。然后刷新 Hibernate Session 将自动更新 UserHasRibbon 表,因此不需要专门的 DAO。

这是一种简单的方法,显然可以改进。一个明显的改进是对您正在检查的功能区更具选择性。也许每个 Controller 方法都可以通过检查是否有任何相关的 Ribbons 现在适用来完成 - Controller 应该知道在其操作之后可以授予哪些 Ribbons。

希望对您有所帮助,如果我完全错过了重点,请告诉我,我会再试一次。

于 2012-06-20T20:26:45.120 回答
0

Why use DAO in the DomainObject? I suggest to decouple DAO and DomainObject, because (IMHO) the method isEarned(User user) is not relevant for First2000UsersRibbon.

class UserHasRibbonDao {
    public boolean isEarned(User user){
        if(!userHasRibbonDao.userHasRibbon(user, this)) {
        // TODO
        // All the logic to determine whether the user earned the ribbon
        // i.e. check whether the user is between the first 2000 users who registered to the website
        // Other autowired DAOs are needed
        } else {
           return true;
        }

        return false;}
}
于 2012-07-12T03:50:45.333 回答
0

您还可以尝试在First2000UsersRibbon类上使用@Component注释声明以及 @Entity 注释。并确保具有此类的包在. 除此之外,您还需要确保该类的对象不是使用运算符创建的。<context:component-scan base-package="" />new

希望这对您有所帮助。干杯。

于 2012-06-19T07:32:09.723 回答
0

我认为你需要调整你的设计。我的第一个问题是“你的 Ribbon 类为什么能够检查哪个用户拥有它?” 这就像说厨房的桌子应该有一个名为boolean doesThisKitchenHaveMe(Kitchen k).

对我来说,您需要将功能区映射到用户的第三个定位器服务似乎更合乎逻辑

于 2012-06-22T16:05:05.377 回答
0

正如 Alex 已经提到的,将应用程序实体作为 bean 在您的上下文中并不是一个好习惯。有很多麻烦的事情会发生,看起来不像是一个好的设计。

代码将如下所示:

public abstract class Ribbon{

    public abstract boolean checkUser(User user);
}

public class NewUserRibbon extends Ribbon{

    @Override
    public boolean checkUser(User user){
        // your logic here
    }
}

在您的服务中,您可以拥有系统中所有功能区的缓存集合(除非它们是动态的),我建议甚至按事件触发器(新用户、答案、投票等)对功能区进行分类,这样您就可以通过与当前用户一起迭代适用的功能区列表,只为适当的功能区(而不是所有功能区)检查您的服务。

于 2012-06-20T02:07:13.423 回答