Question

I have lists of product and categories and need to know if each user has viewed them, also total number of products for each category that have been viewed by each user.

In short, I need to keep lists of

  • Products that each user viewed (Once a user viewed a product's profile it will be added to this list)
  • Categories that each user viewed (Once a user viewed a category's profile it will be added to this list)
  • Number of products in each category that have been viewed by each user?

I've added two member variables to Client class as following. I am not sure if it is correct or not, secondly I do not know how to keep a counter for all viewed products of each category per user. (I know I can retrieve it using (COUNT) function of database but I am wondering if there is any other efficient way rather than sending this request to database).

Client:

@Entity
public class Client {
  @Id
  @GeneratedValue
  private long id;

  @OneToOne
  private List<Product> viewedProducts = new ArrayList<Product>(); //keep list of viewed products

  @OneToOne
  private List<Category> viewedCategories = new ArrayList<Category>(); //keep list of viewed categories

  ....
}

Product

@Entity
public class Product {

  @Id
  @GeneratedValue
  private long id;

  @ManyToOne(cascade = CascadeType.ALL)
  private Category category;

  .... 
}

Category

@Entity
public class Category {
  @Id
  @GeneratedValue
  private long id;

  ....
}
Was it helpful?

Solution

My suggestion is to keep those member variables that you have at the moment, and add the following to your class

   @OneToMany
   private List<ViewedProductsPerCategory> = 
                                    new ArrayList<ViewedProductsPerCategory>();

in that class you should have

@Entity
public class ViewedProductsPerCategory {
   @Id
   @GeneratedValue
   long id;

   @OneToMany 
   Category category;

   long CatCounter;

   long ProCounter;

   getters and setters;
}

BUT make sure to keep the counters in cookie rather than calling database every time that user visit a product or category. After user's logged out add the numbers in cookie to the respective counters in database.

Issue: Be aware that user may delete the cookie before logging out, although it is very rare as most users do not delete their cookies while are logged in to avoid being logged out.

OTHER TIPS

First, you should use @ManyToMany annotation for viewedProducts and viewedCategories. Second, how many products are there? If hundreds or thousands, you may need to do paging through viewed products, or fetch some of them by context or whatever else. When you have collection of viewedProducts, you deal with all the viewed products loaded into memory from database which leads to performance loss. So you would better to create objects like these:

@Entity public ViewedProduct {
    @Id private Integer id;
    @ManyToOne
    private Client customer;
    @ManyToOne
    private Product product;

    ViewedProduct(Client customer, Product product) {
        this.customer = customer;
        this.product = product;
    }

    public Client getCustomer() {
         return customer;
    }

    public Product getProduct() {
         return product;
    }
}

@Entity public ViewedCategory {
    @Id private Integer id;
    @ManyToOne
    private Client customer;
    @ManyToOne
    private Category category;
    @Basic
    private int viewedProductCount;

    ViewedCategory(Client customer, Category category) {
        this.customer = customer;
        this.category = category;
    }

    void incrementViewedProducts() {
        viewedProductCount++;
    }

    public Client getCustomer() {
        return customer;
    }

    public Product getProduct() {
        return product;
    }
}

public class ProductViewService {
     private ViewedProductRepository viewedProductRepository;
     private ViewedCategoryRepository viewedCategoryRepository;

     public ProductViewService(ViewedProductRepository viewedProductRepository,
             ViewedCategoryRepository viewedCategoryRepository) {
         this.viewedProductRepository = viewedProductRepository;
         this.viewedCategoryRepository = viewedCategoryRepository;
     }

     public void viewProductByCustomer(Client customer, Product product) {
         ViewedProduct vp = viewedProductRepository.find(customer, product);
         if (vp == null) {
             vp = new ViewedProduct(customer, product);
             viewedProductRepository.add(vp);
             ViewedCategory vc = viewedCategoryRepository.find(customer, product.getCategory());
             if (vc == null) {
                 vc = new ViewedCategory(customer, product.getCategory());
                 viewedCategoryRepository.add(vc);
             }
             vc.incrementViewedProducts();
         }
     }
}

public interface ViewedProductRepository {
    ViewedProduct find(Client customer, Product product);
    void add(VIewedProduct product);
    List<Product> getRecentlyViewedProducts(Client customer, int limit);
    // etc
}

public class PersistentViewedProductRepository implements ViewedProductRepository {
    private EntityManager em;

    public List<Product> getRecentlyViewedProducts(Client customer, int limit) {
        return em.createQuery(
                "select vp.product " + 
                "from ViewedProduct vp " +
                "where vp.customer = :customer " +
                "order by vp.id desc", Product.class)
                .setMaxResults(limit)
                .getResultList();
    }

    // etc
}

for counting products you should decide, whether denormalization is really needed. If it is really doesn't, you can throw away viewedProductCount and use JPQL queries to get this quantity as needed.

If the number of views for products by customer is not actually required by your application, but instead is a metric needed by the business/marketing to gauge customer interest, then I don't think you should put that logic into your application. There are several non intrusive ways you can provide that information without bugging down your app with constant DB writes on every page view.

Depending on how your logging is done, you could setup access logging like so:

2013-12-03 17:31:44 user:username@example.com INFO  com.acme.web.interceptors.AccessLogInterceptor  - URL [/product/1234]

Then using log analyzer tools (I user Heroku which has Logentries add-on) to gather your business intelligence information. This also avoid you having to write code to display/export the data, it's all already built into the tool.

Another option is using Google Analytics, it gives you much more detailed information about user's browsing patterns, drop off rates, etc.

It would make things simpler if we add to the domain model a couple of entities corresponding to some concepts that could be missing from the model.

If this a marketing/sales domain it would be better to add some new entities such as ProductViewEvent and CategoryViewEvent for the first two points, that point to Category and Product and contain all the properties of the view event.

The Product and the Category themselves do not need to reference the event entities, it would be a unidirectional relation from the event entity to Category/Product.

Depending on the domain these event entities could even become value objects, are two view events completely interchangeable from a domain point-of-view?

For the third point, add a new service StatisticsService that gives the grouped totals you need on a service method.

For the running totals, if it causes a performance problem running the COUNTs on the database, it's always possible to add an attribute with the running total of views for Category/Product.

But update these running totals with batch update queries at the service layer of the form update total = total + difference where ... to ensure the totals are kept consistence from a database transaction point of view.

It's better to not use this optimization unless it's necessary, this depends on the expected volume of views. do you have an idea if it's on the thousands, millions range?

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top