Making a unified tagging system with many to many ecto relations

This is the most liked video Alchemist Camp has ever published on YouTube. I hope you enjoy!

  • Series Overview - Planning
  • Part 1 (this page) - Setting up schemas, contexts and ecto relations
  • Part 2 - Editing multiple tags at once as a list
  • Part 3 - Listing mixed forms of content together for topics
  • Part 4 - Setting up meta tags for SEO
  • Part 5 - Making content-specific markdown extensions
  • Part 6 - Using ETS to cache information and speed up the site

(Source code available for premium members)

The plan

Note: This video was recorded with Phoenix version 1.3! While the changes between versions 1.3 and 1.4 are very minor on the back-end, some generators create slightly different output. I suggest using the v1.3 installer to create the app for this tutorial. Run the following before starting:

mix archive.install

Today we're taking on a somewhat larger task. We're building out some CMS-like features for a Phoenix app. The goal is to make it possible to publish multiple types of content—starting with blog articles, screencast episodes and resource links. Furthermore, we want to handle these types of content in such a way that they can be tagged with topics and liked by users in a unified way. Finally, we want to set up some properties common to all types of content, like whether or not it has been published and whether or not they are restricted to logged-in users.

Basically, the content-related portion of our schema will look like this:

  • entities
    • published
    • requires_login
    • is_premium
    • view_count
  • entity_likes
    • entity_id
    • user_id
  • articles
    • title
    • slug
    • content
    • entity_id
  • episodes
    • title
    • slug
    • content
    • seconds_long
    • video_url
    • entity_id
  • resource_pages
    • name
    • slug
    • content
    • resource_url
    • entity_id
  • topics
    • text
  • entity_topics
    • topic_id
    • entity_id

Generating our schemas and contexts (3:46)

The first thing to generate is a Context for content and a schema for our entities (which will act as a "parent" table for articles, episodes and resources). After that, we'll use the full html generators for articles, episodes and resources and edit their generated schemas to add a belongs_to :entity, Entity and edit their migrations to add unique indexes for their slugs. We'll also generate a schema for topics, which will consist of just the text label of the topic.

Don't forget to add the routes the generators say to for each of the html generators. Running mix will fail otherwise. After this it should be possible to create an episode via the generated crud templates. It work and then we drop in a pre-styled front-end template and CSS (since that's not the focus of this episode).

Tying together the associated schemas (18:00)

The episode we just created will break in our new show template. That's because the show template copied in rightfully assumes that every episode belongs to an entity, but our generated CRUD interface couldn't enforce or handle that. So let's poke around and how to do this from iex. After working out exactly how to create entities and then associate them with episodes as we create them, we can put the exact same logic into our content context that our controllers are already using.

Implementing tagging and likes (23:14)

Tagging content with topics is considerably more complex, and the work falls into the following phases:

  • Create a join table between Topics and Entities
  • Create a many-to-many relationship between topics and entities
  • Enforce the uniqueness of each topic/entity pair in the join table (i.e. "no duplicate tags")
  • Create helpers in our Content context to make it easy to add or remove tags to an entity
  • Extend the helpers so that tags can be added to any struct that contains an entity
  • Implement similar helpers for removing tags
  • Update our list_<content> and get_<content>! helpers to preload tags
  • Update our templates to display topics

Likes work the same way as above except that they relate entities with users rather than with topics.

Note that with how our migrations are set up, an error will be thrown if you try to delete topics that are currently being used to tag episodes (or other items with entities). If you prefer to have other behavior then change the on_delete option in your entity topics migration

add(:topic_id, references(:topics, on_delete: :nothing))

from on_delete: :nothing to on_delete: :delete_all or on_delete: nilify_all.

Work to be done

In the follow-up episode we'll improve our forms to allow mass editing of tags on a given piece of content, and unified views of all content with a particular tag will come after that.

Back to index

No Comments