Ruby Unconf 2019

I can't believe it's not an attribute!

Ruby Unconf 2019

00:00:04.390 I hope everyone is ready for the next talk. Everybody is ready to know why Stefan cannot believe it's not an attribute, so please give a warm welcome to Stefan.
00:00:43.070 I started working with Ruby on Rails in 2008. Over the years, I've come across several recurring problems when implementing certain features in Rails applications, especially when using the database. The first issue is having something like application-wide settings.
00:01:00.120 One could argue that this is a good or bad pattern, but what I often see is a changing application name that can include things like an Easter egg on Easter or a holiday theme. For instance, on Christmas, the application name may change, or things like a contact email may change over time. There might also be a Google Place ID or styling for things like Google Maps that can be altered in the database.
00:01:21.960 So, what do you do in these cases? You create a table and call it 'application_settings' with a name and a value. Then you can create all these nice settings, and you have the ability to change them over time. This way, you have a single-purpose key-value store for your application-wide settings.
00:01:45.240 However, once you have a lot of settings, it can become somewhat messy, especially when you consider user-specific settings. For example, let’s say we have an application that consists of a lot of table views, which is still pretty common. If we have a user list with dropdowns asking how many items one would like to see per page, it becomes annoying if the system doesn't remember the user’s previous choice.
00:02:08.280 Next time, after I chose 30 items to show per page, it defaults back to 10. So, what you might do is have a user table where you add a column for 'users_per_page' that defaults to 10. I can update this, and the next time the user comes back, I can set it to their desired count. This looks okay for now, but as you can imagine, if we add another setting like 'posts_per_page,' and keep adding more, it could become a bit messy in the user table.
00:02:43.800 So, what do we do as good database architects? We can use the often-discussed serialized function in Ruby on Rails. Everyone knows what 'serialize' does in a model, right? In brief, you add a text field to your database, and after that, you can store whatever object you want into this field, as long as you write 'serialize my_field_name.' It's automatically serialized, and once retrieved from the database, you get your array or complex object back.
00:03:06.090 You might think this is a neat solution, but it’s not the most elegant. You could write some helper methods to get around this needing to know that it’s a serialized attribute. After considering that, you might think, if I need all these settings, why not create my own model and my own database table called 'view_count_settings'? Now we have general settings, application settings, and view count settings to manage.
00:03:45.470 But then, you end up with one table in your application that is specifically there just for saving how many items in the table each user would like to see. This seems a bit wasteful. To recap, I prefer not to clutter my database with 'application settings' or additional columns in the user table, nor do I want a table that only counts how many items the user wants to see.
00:04:05.720 At my current job, where I work for the guys who provided the coffee this year, we frequently encounter requests for temporary features. For example, if we need to track how many users a specific user has invited for a campaign next week to reward them in return. But in this case, we don't want users to have only five reactions; we want them to have more.
00:04:41.180 These things are often temporary; something that might get removed after a presentation or a weekly sprint. What options do I have? I can add something to a table, create a new model or a table, or just start serializing. Again, the same dilemma arises—why should I introduce migrations into my codebase if I know I will probably have to revert them a week later?
00:05:14.160 As time passes and these problems recur, they can become quite annoying. So what to do in the end? The previous solutions I suggested are not bad, but I don't want them to be in my application duplicated 20 times. I thought of a solution that utilizes all three methods but only once in my codebase.
00:05:43.750 We take the first approach and create settings. We refer to it simply as 'settings'—essentially, a key-value store with a name and a value. Additionally, I allow these items per page to be assigned to another record, specifically to a user in my application. Now, I can create a setting named 'users_per_page' with the value 30 and assign it to User 1.
00:06:43.270 So, what came out of this? I don't know if the name still matches the idea, but I call it 'setting accessor.' It provides a global key-value store with a nice interface. You can set the meaning of life and read it again with different forms of syntactic sugar.
00:07:06.950 Under the hood, this creates a setting that is not assigned to any specific user or record, allowing for extensive usability in applications—whether for the title, contact address, etc. There was a talk yesterday that explicitly avoided using 'method missing,' but my approach relies heavily on it.
00:07:41.270 As I mentioned, you can assign different records in your application. If we have multiple universes, one could use 42, but for my second application, let's say I need it to be 43 because it's essential. This can be done easily.
00:08:07.050 The syntax remains largely the same, with just one additional attribute. Here’s a short example of how I actually used this to set items per page for various views in my application. This is particularly interesting for those of you who are into Rails. The controller path is created automatically. For instance, a 'users_controller' would have 'users' as the controller, and the action name could be 'show' or 'index.'
00:08:53.190 I just have 'users index' through a join, and when the user selects a different value, I set this setting for 'users index' with the parameters for per page—which in my case would be 30—related to the current user. The next time the user comes back, I retrieve the value again. If the user hasn't set anything yet, I set it to 30, or whatever my default value is.
00:09:57.000 This approach becomes quite efficient for scenarios where I don't know how many different settings I might need. For example, I don't have to worry about managing additional views in my application anymore because it works automatically, accommodating new lists. However, it still feels a bit cumbersome when integrating new features.
00:10:23.360 In cases where I introduce experimental or throwaway features, I require an improved solution that integrates seamlessly into my models. The first line you see, 'setting accessor,' is meant to trick Active Record into treating this as an actual database column.
00:10:40.010 Every user now has an attribute called 'invited_users' of type integer, which defaults to zero. You can see that I can use it just as I would with a standard database column. I can set it to one, or I can increment it with 'plus equals' one and save it.
00:11:18.970 You also have the serialization aspect I mentioned earlier. You can create setting accessors that are polymorphic in nature, which means they can hold whatever you want—just don't save objects with 30 other embedded objects within them, or your database might complain.
00:11:35.480 In my case, for allowed reactions for a post, I can specify that all posts can have 'happy' and 'sad' reactions by default, but for the first post, I might not want users to express their thoughts, so it might only allow the 'happy' and 'sad' reactions. I can implement this seamlessly, using it as you would expect with an array attribute in Active Record.
00:12:23.170 As mentioned, it behaves like a normal attribute, which means you can call validations on it. Ultimately, you can do everything you usually would with a standard attribute, along with all those cyber methods that you might have or haven't utilized yet. You can change an accessor to ask whether the user changed it or not, what was its previous value, and what it was cast to.
00:13:05.690 As you can see, if I assign a string to an integer, Rails does a bit of magic to convert that correctly. You get all these nice features, and in the end, you wouldn’t even realize it’s not a real attribute.
00:13:38.840 As a result, this allows for experimental features and temporary functionalities without the need for continuous modifications to the database. So, if my boss comes to me next week and says we require this new feature, I can just respond that I need to add one line of code and maybe a couple more to set the value, without needing to modify the database and then revert it later.
00:14:06.970 This isn't a universal solution, but it has been running in production in several systems for four to five years, and thus far, nothing has broken.
00:14:35.860 I've encountered some issues keeping this up-to-date with the changes introduced in Active Record over the years. For instance, Active Record sometimes doesn't behave as intuitively as expected. Let's look at an example: previously, one could assign strings to boolean attributes in their records and wonder if something would evaluate as true or false.
00:15:17.860 In earlier versions, it might be misleading, and this produced confusion with my tests. Luckily, I discovered the use of appraisal, which allows testing against multiple versions of the same gem at once. This makes it safer to ensure compatibility across different setups.
00:15:58.400 When working with Active Record, one tends to expect consistent outcomes—especially when checking if a record is changed. If I ask the system if it’s changed, it should return true, but I hope Active Record 6 doesn't surprise me with unexpected behavior.
00:16:36.300 It’s important that when saving my record, if anything was indeed changed, I expect the updated_at timestamp to also change. Previously, one would automatically touch it upon saving, but since 5.1, Active Record only touches the record if any attributes have changed. I found this update surprising, which required me to adapt my existing practices.
00:17:21.400 However, I have a way to deal with this. In my gem, I create a boolean converter that receives a value and attempts to convert it to a boolean. I can leverage Active Record’s behavior as a baseline and develop around that, ensuring compatibility with potential future updates.
00:17:56.880 I hope all of this demonstrates the flexibility and robustness of my approach, as changes in Active Record ensure that I can keep my functionality intact.
00:18:32.720 Thank you for your attention, and I welcome any questions or discussions regarding the technical aspects of what I've shared. Does anyone have questions about the implementation?
00:18:54.220 Oh, sorry, I should have said something at the beginning.
00:19:02.990 Thank you for your talk; it was excellent! I have a question about performance—how do you manage the need for constant reading of these settings? Currently, I handle it lazily, loading the settings the first time one is requested. I have a wrapper in place to keep track of all settings for a record.
00:20:01.300 What you can do is eagerly fetch them when loading the user, but I haven't implemented that yet. However, it is certainly doable, and you'd avoid having to load them each time.
00:20:28.420 Caching presents a problem—when you have multiple instances of settings, which can lead to issues in managing their coherence. While there’s no perfect or finished solution for that yet, I'd appreciate any contributions or insights people might have about tackling this.
00:21:38.200 I try to implement as little alteration to the actual Active Record as possible, creating methods when I need them. Hence if something changes, I hope it doesn't lead to major issues.
00:22:28.430 Regarding your question on preferences for software settings, my opinion varies by application. If an app has very few settings that effectively cover user needs, that’s fine. However, for apps like Google or Facebook, privacy settings are critical, thus I would prefer extensive control.
00:22:58.730 Implementing a 'pro mode' with extensive options alongside an 'easy mode' that combines multiple settings can greatly enhance user convenience.
00:24:33.550 I have plans to support other ORMs, but currently, there are no specific plans for it since it's not yet stable enough for new development.
00:25:10.200 Is there any danger of the settings table becoming cluttered with too many experimental settings? This has occurred in the past. For example, if a setting for 'invited_user' were to become a real database column, I would allow database migration, simultaneously cleaning the settings table.
00:25:58.650 New features could require migration tasks to cleanly implement necessary settings. On occasion, I create specialized rake tasks that are executed post-deployment to handle these migrations, keeping the process clean and efficient.
00:26:50.030 Queries regarding adding a fourth reaction to posts reveal that if a user has designated reactions, migration tasks would need to be completed to update their settings, but those without specific designs would inherit new defaults without needing individual updates.
00:27:40.610 This type of integration and flexibility is a critical aspect of how I maintain and update settings. I'm always looking for ways to improve how we manage reactions and settings effectively.
00:28:03.200 Thank you all for your contributions and engagement today!
00:28:22.030 [Event Concludes]