What you should know about Elixir - Part #1What you should know about Elixir - Part #1
As Elixir developers @ Coletiv, we periodically come face to face with some unexpected challenges and obstacles in our projects and we believe in sharing our solutions with the community, as we have been doing on some of our blog posts.
However, we feel that there are some interesting Elixir topics, tips, and tricks that do not warrant a whole blog post just by themselves, so we had the idea of starting a periodic blog post, where we share some important and interesting aspects of Elixir and its frameworks.
This is the first of many - we hope you enjoy the content and find it useful!
Quick Tip #1 - Using Repo + Schemas in migrations can land you in trouble
When writing a migration, try to avoid using database functions that depend on schemas, either to fetch or alter information.
If you write a migration that has functions that depend on a schema, and you then alter the schema in the future, the migration you are writing might not be able to run anymore, since it may depend on fields that have been altered or removed, or it will try accessing fields that haven't been added yet.
Below is a more in-depth explanation with examples, in case you're curious. Feel free to skip it if you want.
You have a Users table and schema...
- On a 1st migration - You want to update some user's names, so you fetch them using the User schema, and then update them.
- On a 2nd migration - Immediately after writing the 1st migration, you noticed you needed a new column on the Users table, "last_active", so you add it on a 2nd migration. You also update the schema to add this new field, so that it is consistent.
You now try to run both migrations at once...
- And the first one fails! It will say that the "last_active" field, which now exists on the User schema, does not exist in the database yet. This field would only be added to the database by the second migration, therefore locking you from running the 1st migration at all!
Of course, this is only an example, and it is true you could just run the 1st migration independently, and only then include the 2nd one and run it.
However, in real life, this situation could happen, for instance, if your application has some environment that you only deploy to when you have a collection of changes ready and tested, you might still encounter this issue.
- You developed the 1st migration, ran it locally, tested it. Then you wrote the 2nd migration, ran it locally, and tested it. Everything works!
- It is now time to send your code to your staging environment. You commit your code, and your CI tool attempts to run both migrations... and fails - it is attempting to run both migrations, but the committed code already has the updated schema, leading to the error described above on the 1st migration.
As an alternative, within migrations, we suggest you try to use raw SQL queries as much as you can or using the Repo's insertall, deleteall, and update_all functions, which also ignore schemas.
Quick Tip #2 - How to use a dependency inside a migration
Sometimes you really need to write some custom logic within a migration, and dependencies may be part of this. However, when running a migration, the application does not automatically start any of its listed dependencies.
To do this, you need to start up the dependency yourself within the migration, using the following code:
Quick Tip #3 - Maps and ordering
For practical effects, as a general case, you should assume that maps and any map-based structure in Elixir is unordered.
This implies that you need to be extra careful whenever you convert a list to a map or mapset, since you should never assume the order is kept, even if you previously ordered the list.
In reality, even though this is sound advice, it might be interesting to know that in practice a map's content may actually be shown as in a certain order, but you might want to know that...
- They are only ordered up to 32 elements - this is due to the internal implementation of maps in Erlang, which is different above and below 32 elements. Below 32 elements, it uses a flatmap, above 32 it uses a hashmap (Hash-Array-Mapped-Trie).
- The elements of a map are compared by key in ascending term order, and only then by value in key order.
As for the ordering of keys and values, Erlang term ordering rules apply here:
number < atom < reference < fun < port < pid < tuple < map < nil < list < bit string
Here's an example of the iex console display of a map with the same contents being created, and how it automatically shows them with the keys ordered alphabetically:
Note: Keep in mind that the Erlang documents say that the map elements are printed in an arbitrary order. However, in practice, what was written above applies - feel free to try this out yourself in the iex console!
You can actually use comparison operators (<, ==, >, ...) between maps. From the Erlang map specifications:
Maps are ordered by size, two maps with the same size are compared by keys in ascending term order and then by values in key order. In maps, key order integers types are considered less than floats types.
If there's a problem where you need an ordered structure and you're currently using a map, try to find alternatives, since this is an unsafe practice.
See you next time!
Hope these tips end up being useful to you, as they were to us when they helped us solve some of our past problems. Hope to see you on our next "Elixir - You should know..." article!
Thank you for reading!
Thank you so much for reading, it means a lot to us! Don’t forget to follow Coletiv on Facebook, Twitter, and LinkedIn as we keep posting interesting articles on technologies, processes, and experiences.
If you'd like to work with us on a digital product just drop us a message here.
Do you want to become part of our community?
Join our newsletter 😎