Dymo
For all your labelling and tagging needs!™
Motivations
When it comes to polymorphism, it’s always hard to find a match-all solution. Each known implementation have tradeoffs:
- Real polymorphism (
taggable_id
,taggable_type
on theTaggings
model) breaks database level foreign keys, and makes requests slower. - Parent entity (Having each model have a
entity_id
refering to someEntity
model) and having tags assigned to theEntity
model viaentity_id
is cumbersome when performing complex queries and reverse queries. - Multiple nullable foreign keys on the
Taggings
model (Eguser_id
,post_id
) is a very good solution but it could become hard to maintain and enforce if many models are taggable. - One join table per taggable model (Eg
users_tags
andposts_tags
) make it impossible to consolidate and search using SQL queries only.
This package offers an implementation that fits the 3rd and 4th approaches, and it offers shortcuts and mix tasks to make your life easier.
Installation
If available in Hex, the package can be installed
by adding dymo
to your list of dependencies in mix.exs
:
def deps do
[
{:dymo, "~> 0.1.0"}
]
end
Then, you can install the Tag
migration in your application (Note
that for umbrella apps, you’ll need to first cd
into the app
containing your repo migrations):
$ mix dymo.install
* creating priv/repo/migrations
* creating priv/repo/migrations/20180828154957_create_tags.exs
Once done, you should start and make a join table for the model(s) you want to be able to label. There is a mix task for this too!
$ mix dymo.join_table MyApp.Post
* creating priv/repo/migrations
* creating priv/repo/migrations/20180828154958_create_posts_tags.exs
Once your database gets migrated, a new table posts_tags will be created.
You can add the tags relationship with dedicated macro in your MyApp.Post schema:
schema "..." do
tags()
end
If you follow the directives given by the tasks, you should then have a fully labellable Post model. Congratulations!
Using Dymo.Taggable
When a module uses Dymo.Taggable
, many shortcut functions are
meta-programmed into it.
It becomes easy to achieve labelling-related tasks. All the examples
bellow assyme that a Post
module calls use Dymo.Taggable
.
Editing Labels
To set the tags on an instance of a post:
Post.set_labels(post, ~w(ten eleven))
Similarily, you can add / remove labels using Post.add_labels/2
and Post.remove_labels/2
.
Querying Labels
To get the labels associated with a given post, you have several options.
Using the association directly if you defined it:
post
|> Repo.preload(:tags)
|> Map.get(:tags)
|> Enum.map(&(&1.label))
#
Using the helper function:
post
|> Post.labels()
Note that the Post.labels/1
also accepts a module directly as an input - in that case it would return all labels that were ever associated with posts.
You can also query models that are tagged with specific labels by doing the following:
Post.labelled_with(~w(ten eleven))
Notes
Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/dymo.
Changes
0.3.4
- Adds some more spec.
0.3.3
- Adds some more spec.
0.3.2
- Use protocols for dispatching labelling functions to implementation
- Add generic
Taggable
functions for using theTaggable.Protocol
protocol
0.3.1
- Add support for
:binary_id
primary key for taggable structures
0.3.0
Dymo.Tagger.query_labels/2
becomeDymo.Tagger.query_all_labels/{2,3}
. See below for semantic of third argument.Dymo.Tagger.query_labels/3
becomeDymo.Tagger.query_labels/{3,4}
. See below for semantic of third argument.This version adds support for namespaces. Namespaces allows to isolate labels.
Dymo.Taggable
generated functions can take as additional argument a namespace on which to apply the function.WARNING: no namespace means ~root~ namespace, not all namespaces.
set_labels/3
: set all tags for a given namespace, keeping others untouchedadd_labels/3
: add labels to the given namespaceremove_labels/3
: remove labels from the given namespaceall_labels/1
: returns all labels of the given namespace for a modulelabels/2
: return all labels for the given struct and namespace
0.2.0
- Add compatiblity with Ecto 3.x
0.1.x
- Compatible with Ecto 2.x