Evolution of a Django Repository pattern

Posted in: Django, Python
  1. First attempt - get product by primary key:

    class ProductRepository:
        def get_by_pk(self, pk):
            return Product.objects.get(pk=pk)
    
  2. ProductRepository is stateless, use static methods. Usage now looks like:

    ProductRepository.get_by_pk(pk)
    
  3. It turns out I need a ‘get by slug’ too:

    ProductRepository.get_by_pk(pk)
    ProductRepository.get_by_slug(slug)
    
  4. In a web context, I need to limit according to user because not all products are public yet:

    ProductRepository.get_by_pk(pk)
    ProductRepository.get_by_slug(slug)
    ProductRepository.get_by_pk_for_user(pk, request.user)
    ProductRepository.get_by_slug_for_user(slug, request.user)
    
  5. Need some list APIs as well as individual:

    ProductRepository.get_all()
    
  6. And to limit by user sometimes:

    ProductRepository.get_all()
    ProductRepository.get_all_for_user(user)
    
  7. Need to limit to certain brands, for both list and individual. Now I’ve got:

    ProductRepository.get_by_pk(pk)
    ProductRepository.get_by_slug(slug)
    ProductRepository.get_by_pk_for_user(pk, user)
    ProductRepository.get_by_slug_for_user(slug, user)
    ProductRepository.get_by_pk_for_brand(pk, brand)
    ProductRepository.get_by_slug_for_brand(slug, brand)
    ProductRepository.get_by_pk_for_user_for_brand(pk, user, brand)
    ProductRepository.get_by_slug_for_user_for_brand(slug, user, brand)
    ProductRepository.get_all()
    ProductRepository.get_all_for_user(user)
    ProductRepository.get_all_for_brand(brand)
    ProductRepository.get_all_for_user_for_brand(user, brand)
    
  8. Aargh! Refactor:

    ProductRepository.get_one(pk=pk, for_user=user, brand=brand)  # slug=slug also allowed
    ProductRepository.get_many(for_user=user, brand=brand)
    
  9. Need paging:

    ProductRepository.get_many(page=1, page_size=10)
    
  10. But have to specify ordering if paging is to work:

    ProductRepository.get_many(ordering='name', page=1, page_size=10)
    
  11. Hmm, performance - sometimes I need to fetch other things at the same time:

    ProductRepository.get_many(fetch_related=['brand', 'stock_info'])
    
  12. Hmm, my related things also need related things at the same time:

    # TODO fix this performance problem in the next release, honest!
    
  13. Extra flag needed to only show products that are in stock:

    ProductRepository.get_many(in_stock=True)
    
  14. Fetch the products in user’s basket only:

    ProductRepository.get_many(for_user=user, in_basket_for=user)
    
  15. Hmm, I have a lot of parameters now:

    class ProductRepository:
         def get_many(
           for_user=None,
           fetch_related=None,
           ordering=None,
           page_size=None,
           page=None,
           brand=None,
           in_stock=None,
           in_basket_for=None,
       )
    
  16. Idea 1 - Filter object:

    ProductRepository.get_many(filter=InStock())
    ProductRepository.get_many(filter=InBasket(user))
    
  1. Idea 2 - switch to a Fluent interface:

    ProductRepository.for_user(user).filter(InStock()).fetch_related('brand', 'stock_info')
    
  2. Advanced ordering:

    ProductRepository.for_user(user).order(OrderBy('price', 'product.name'))
    
  1. Finishing touches - [x:y] slicing:

    ProductRepository.for_user(user)[0:10]
    
  2. Enlightenment:

    Product.objects.for_user(user)
                   .in_stock()
                   .by_brand(brand)
                   .order_by('price', 'product__name')
                   .select_related('brand')
                   [0:10]
    

Postscript

For those who don’t know the context, I’m suggesting you should just use Custom QuerySets as your “service layer”, instead of a hand-coded repository pattern. See also Against service layers in Django.

Also, it’s worth noting that the evolution of QuerySets in Django itself wasn’t so different from some of these steps.

Comments §

Comments should load when you scroll to here...