Friday, November 20, 2020

Luke Plant: Evolution of a Django Repository pattern

  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.



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...