Skip to main content

Virtual Attributes And Rails 5 Attribute API

29

Episode 98 · December 30, 2015

Learn how you can use virtual attributes to make forms cleaner and how the Rails 5 attribute API can save you a bunch of time

Rails 5.0 ActiveRecord


Transcripts

There's a lot of new features in rails 5, and one of them I want to talk about, that has been talked about a little bit less than other things like ActionCable, is the attributes API. This is useful not just for virtual attributes, but attributes in general. When you want to cast things that come in through the params to a certain type in ruby. This is super cool, and something I wanted to talk about, because yesterday I was talking with a friend of mine, helping him refactor some code that he had hacked together around the rails 4 and previous implementations of virtual attributes. In this episode, we're going to talk about what he was doing, why this is important, and then how you can do this in a new rails 5 functionality here. Let's dive into that and a little bit about virtual attributes, what you might use them for and let's get started. Let's make rails 5.0.0.beta1 new payments. We'll call it payments because payments have a lot of good examples that we can use here to talk about virtual attributes, how they're useful, and then how we can go about actually making them more seamless in our application. Let's cd into the payments application. Let's just generate a scaffold for a charge model. The charge is going to be a model that just saves the user's credit card charges, so anytime that a user would get charged, we would create a Charged object. This is very similar to Stripe, and we're going to need to store an amount on there of how much the card was charged. Mount is interesting because, and we're going to follow Stripe's lead here and say: Let's store the amounts in cents as an integer, and that helps you deal with the routing errors that can crop up if you're storing the amount in a decimal or a float format. They store as an integer, and that requires you to save 1000 in the database for a 10 dollar amount. That becomes a little bit harder to deal with when you are allowing your users to create charges in your UI. Let's go ahead and create that scaffold, and then rake db:migrate that, and then take a look at this in the browser to see what a virtual attribute could help with when we're building out the form_for charges

rails g scaffold Charge amount_in_cents:integer

rake db:migrate

Now that that's done, I'm going to cd into payments, and then run our rails server, and then, once that's loaded, we can fire up localhost:3000/charges. When you're creating a new charge, you have to type in the amount in cents, and that's not something that is really useful for users. They don't really process payment amounts in cents, they process it in decimal format with two decimal places after the dot. That is something that we have to take into account when building the UI. We actually don't want users to type an amount in cents, we want them to type in the dollar amount as they're used to, so we actually want that formatting and everything in there. That becomes sort of problematic. How do you go about fixing that? One of the ways that we could do that is that you could write some JavaScript, and you can modify this form field. We could open up our code here, and we could say: Let's actually go to the charge, let's set a virtual attribute, and this will be attr_accessor :amount, we'll set that, and that will allow us to go into our form, our charges form, and then, rather than filling out a number field for amount in cents, you could have a text field here for amount, and then that would allow us to say: Let's go edit our charges controller, and we'll permit the amount, and then, if we go back to the charge, we can have this convert before validation

app/models/charge.rb

class Charge < ApplicationRecord 
    attr_accessor :amount

    before_validation :convert_amount_to_cents

    def convert_amount_to_cents
        self.amount_in_cents = amount * 100 if amount.present?  
    end
end

The amount is a virtual attribute, it is just a helper to make our fields in the form eat more easy, and then we'll take the value that it saves to that, and then convert that to cents. If we were to go back and refresh this, we now have an amount field, we could type in ten dollars, you can create the charge, and then, we want this to be amount in cents as a thousand, but as you can see, that didn't actually work correctly. We have to do all this work, and it's kind of fragile because we need to be careful with that, what's happening is you're going to get this times 100, so we're doing our console, we would say: Ok, our code in this before_validation is saying: Well, the amount is present, so let's do amount * 100, if we did that, that would definitely be bad. That wouldn't be good at all. That means that we need to make sure that we convert this to float, so then we could get closer, and then we'll assign that to the amount in cents equals 1000, and then rails will convert that to an integer, and it's kind of a mess. You have to be very careful that you're doing this correctly, so you have to make sure that you're converting that to the amount in cents, and if we were to go back and create another one and say: Let's do 15 dollars, create our charge. Now we've got that working, but just last time, we didn't have the .to_f, that was the only thing we changed, and it only saved 10 cents in the last one, so we need to be very careful with that. It can become a little bit nasty, and you have to do a before_validation or something else, this doesn't even work properly because if you do a before_validation, this will get converted after validation, and that's good, but you really want to override when this amount gets set. We actually would want to change this, and we would say attr_reader :amount.

attr_reader and attr_accessor, if you're not familiar, define methods for you. Here's all the options that you have

attr_reader :amount
attr_writer :amount
attr_accessor :amount

attr_reader will generate

def amount 
    @amount
end

attr_writer will do:

def amount(new_value)
    @amount = new_value
end 

Your reader will read from the same variable that your writer sets to, and then, if you were to remove these and just do attr_accessor, it will define both of these for you. That's useful, and that would actually save us some trouble. If we go back to where we're at with the before_validation, we're only doing this before the validation, so anytime before that, amount_in_cents is going to be nil before we try to save and do validations.

If you were to create a new charge here Charge.new(mount: "10.00"), when you look at it, you're actually going to get amount_in_cents as nil, so you can't actually operate off of it immediately. That's because we're setting this attr_accessor method, and we're setting the amount value, but this only gets called right before validation. If you were to say c.valid?, and then print out c again, you'll see that that amount in cents does get set properly, but we can't always trust that we can access it. This wouldn't actually work as code that you would want to write for that. You would actually need to do something more like an attr_reader here, and that's going to generate your amount method, and that's going to print out the amount instance variable, and we would want to create our own setter or writer method, so that we could set the amount, so then here we would say

def amount(value)
    self.amount_in_cents = value.to_f * 100 
end 

This would allow you to basically do the same thing, but this will get set immediately. That is just regular old ruby code, and if we reload here, and we go back up and we run that same thing, you'll see that amount_in_cents gets set immediately. That's just your regular old ruby class functionality, the attr_reader and the attr_writers an the accessors, but we have to override the writer because we want to cast that and do some conversion to it. Even this, you want to be super careful with money, this is a bad example of that, but take a look at this.

You're doing a new charge: Charge.new(mount: "19.99"), which is a very common amount to charge someone's card, the amount in cents gets converted to 19.98, so you have to be very careful. This is a perfect example of why you do not want to use floats for money, if you take 19.99, and that seems correct, and then you multiply that by 100 as an integer, or a float, it doesn't matter, you're going to get an inaccurate value, so you're going to get 19.99999999999998, and that's going to of course round up, you would think, but when you convert this whole thing to an integer, you're going to get 1998. You have to be very careful with that, and you don't want to do hacks and stuff like rounding and whatever. Ideally you don't want to, because you then have to decide what happens if this is 9999, do I round up and then up again, or do I round down, because if you had 1998.49999999998, you could call round on that and round it down, but it's like half a cent, and then what do you do? You have to worry about all that stuff, be careful when you're using floats. That's an example-- We, in this case would have to make our own methods here, in order to help rails more smoothly convert the input on the field from this into something valuable. This is also problematic here, where you go to edit the charge, and if we edit it, the amount is empty. What's happening in here is that you're instantiating the charge again, but the amount is nil by default, so we actually need to ignore that as well, and we have to say that

def amount 
    amount_in_cents.to_f / 100 
end 

We have to do the opposite of this, so that we can even get the amount to show up in the form properly, and even then, it's a float, so it strips off extra trailing zeroes, so you still will have to do some work on the front end to make that virtual attribute work correctly, but you are kind of forced back into using rails itself or ruby itself to help rails interpret that stuff. This is something that is problematic, it's really just like regular old ruby code that you would write as a programmer, and this kind of stuff happens inside of rails, and that's what my friend and I were talking about last night. He had basically a model like this rails g scaffold Event starts_at:date he wanted the ability for you to create-- Let's migrate this and then let's take a look at that in the UI and explain it there. He wanted the ability to create events, and that will work and everything, but he wanted the ability to create multiple recurring events. If you want to make recurring events, then you would just go in here and you can add an attr_accessor, and you'd say:

app/models/events.rb

 class Event < ApplicationRecord
    attr_accessor :recurring 
    attr_accessor :recurs_until 
 end 

app/views/events/_form.html.erb

<div class="field">
    <%= f.check_box :recurring %> 
    Recurring
</div>

<div class="field">
    <%= f.label :recurs_until %>
    <%= f.date_select :recurs_until %>
</div>

This will have a little check box, and we can click that, and then this would allow him to create. If we do January 2016, then basically it will take this date, start there, and then add days until it got to this and create a whole bunch of events. This is cool, but it becomes sort of a problem, because he wants this recurs_until to be a date, and this needs to be a boolean. That's just kind of tough because attr_accessors can actually be nil, there's no validations, they don't really get cast, so you get the values that rails parsed in the params. That becomes a little bit troublesome, let's go into

events_controller.rb

def event_params 
    param.require(:event).permit(:starts_at, :recurring, :recurs_until)
end 

Here we've got our byebug, and we've just created that event, and now we don't get to see those obviously in the event object output, so we can actually, we have to access them directly, and we get it recurring, but it's this string that came straight from the params. This isn't a boolean, so we actually have to say: Is that present or not? Because if we say "continue", and then we go create another one of these, and leave it "false", we're going to submit it and then this time @event.recurring will be 0. You have to deal with that and you have to write-- You can't use attr_accessor and handily put those in, because that becomes-- you know, we have to cast this to true or false, rails does that for us if it knows that it's true or false as a boolean field in the database, but because these are fake fields, it doesn't know, so it doesn't do any of that magic of useful typecasting for us. That's sort of the magic that people are always like: Whoa, it just does that for me, and it's not really magic. That's what we did last night, is we went into rails source code and took a look at that. We just went into rails/activemodel/lib/active_model/type and here's all the different types that it supports, and we were looking at dates, so we'd go into date, and we just look at how it converts the value, so if you look at cast value and say: Well, if you gave me a string, we're going to return nil if it's empty, it's a blank string. Otherwise we're going to try and call this fast_string_to_date, and if that doesn't work, we're going to fall back to string_to_date method, this is a really fast way of doing it, and then this is the old parsing method that you can do. Basically this new date is all we're doing, so we're just passing in year, month and day and creating a new date, and that's it. The conversions in type casting is actually super duper simple for all of these, and all this stuff in here is really just to take those params, for example this is the date params, you have the recurs_until, and it has three parts: year, month and date, and the parts are denoted by one two and three here, and then that I, if you've ever wondered what that stands for, that means that this should be converted to an integer, and so then that will convert these to a hash basically for the recurs_until format, so check out this. We have the recurs_until value, and this is what rails parsed out to be. It knew to take these three key values to take the name, then set that recurs_until attribute on the event, and then it parsed out a little hash and said: Well this should be the key, and this should be the value, and it's already converted all of those to integers for us, and then all it's saying in rails typecasting when you go into date the fast_string_to_date is going to grab the first thing, the second thing and the third thing, and then call new_date on it. All it's really doing is-- you're more or less saying .values, you get all of those in order, and then you say Date.new *event.recurs_until.values and voila, you have that new date.

This is pretty nifty, and allows you to do that stuff, but this is like stuff that you have to do yourself, you would have to go through and write that code in your attr_accessor here, so you would have to do

app/models/events.rb

class Event < ApplicationRecord
    attr_accessor :recurring 
    attr_accessor :recurs_until 

    def recurs_until=(value)
        @recurs_until = Date.new(*value.values)
    end
end 

If we go back to our code and try this one more time, so let's create a new event, let's do January 2016 again, mark this as recurring, and create the event. This time, if we ask what the recurs_until date is, we'll actually get a date object. What we just did, and by implementing these two lines, is we created a virtual attribute, and then did the typecasting that rails does. Rails actually did the parsing of these strings in the params for us and assigned a hash to our virtual attribute right here, so it gave us that hash. Rails did a little bit of the conversion, but we did the actual conversion into the proper object type, and then our attr_reader just reads this variable out, and voila, you've recreated in about five minutes the casting functionality that rails has. Obviously rails, in this date type thing here does a little bit more, and it has some other helpers because it can run the fast one, or it can run the fall back and all of that, like the parsing. If you ever pass in a different format, it could try that as well, so it's like looking for the year, the month and the day and everything. It can do a little bit more, but it's really just extra options on top of that, like basic functionality which we recreated a core piece of ActiveRecord right here in just about 30 seconds. This is cool that you're able to go ahead and do that, and it's not really that complicated. That was what we were talking about yesterday and inspired me for this episode, it's like: If you actually need to cast these values to certain types so that you can interact with date objects, well then cool. That's not that hard, but it's kind of annoying to have to do this every single time, because now we have to do the same thing with this, and we have to make:

def recurring=(value)
    @recurring = case 
    when "0"
        false
    when "1"
        true
    end
end

You might need to handle other types of values, because you might assign that in the rails console, so this is one place they have to worry about, is when the browser sends data over, but you also have to worry about every single time that your user wants to create a new event and pass in recurring as true. When a developer does that, you want to handle that, so you want to not have it where they pass in true and you get false, you need to make sure in those cases you can handle that. You have to do all this conversion and stuff, and it's just kind of frustrating because rails has all of that built in inside of this ActiveRecord type section, so if you look at the boolean stuff, you can see: Oh, here's all the false values that they allow, and it really just checks to see if the value is empty string then it will be nil, and otherwise, it just looks to see if the false values include that, or do not include it. This just converts the include returns a true or false, and because you're looking to see if false includes the value and then not, it will return true or false and all of that. That's all the typecasting is doing inside rails. This is already built for you, and you shouldn't have to go through and hand code your own, and you're probably going to make mistakes and won't be as robust as something like this, and of course, you could copy this into your application, or you could like reference this boolean class and use it to access it, you've got this cast value method that you can try and access and use here, but why should you have to do all of that work, and dig into ActiveRecord to figure out how we can just make this virtual attribute a boolean and this one a date? Wouldn't it be nice if you could just say: This is a boolean and this one should be a date? That's what the new ActiveRecord API does. That's a really good example of that, so there's a not easily accessible documentation yet because a lot of comments here, which are useful, and so it's showing sort of the same thing. If you actually want to override the format in the database, maybe your database has a decimal for price and cents, and you want to override it, and you want it to be an integer in your application, then it could convert that for you, so you could have these decimal objects in the past, and now you'll be able to convert them into integer objects, so you can use this with existing things, not just virtual attributes, but all of that work we just did can be adjusted and you can say: Let's use the attributes, and this should be a string, that should be an integer, that should be a float, and that means we can go and say attribute :recurring, :boolean and then attribute :recurs_until, :date and we can get rid of all of this code, we can go back and let's create a new event. If we do recurring, let's put that byebug back in so we can see this, and we go back to January 2016, create the event, and not in this one, but in our rails console.

It doesn't put it in the output again, because they're virtual attributes, but event.recurring is now a boolean. Rails built in typecasting functionality is actually being used and given to you in a very very easy manner that allows you to add virtual attributes to cast the original attributes. All of that stuff is just handled for you. It's not that complicated to do yourself, but you can take advantage of all the rails stuff with this helper that they basically have added in rails 5. I though that was really interesting, and explains a lot about virtual attributes and the benefits you can get, but it just exposes like a piece of rails 5 that was already there, that you can get access to, now really really easily without having to dive into ActiveRecord itself like we were doing last night, reading the source code for the typecasting.

I hope you enjoyed that episode and the look into virtual attributes and the new attributes API built into rails, and if you're interested in seeing more of this stuff about how things work behind the scenes, basically taking a look at all those dark corners and understanding how websockets works, and http works, and rails works internally so you can get a better understanding of how to interact with all of this stuff let me know in the comments below, because I've been thinking about doing a series of videos basically building up a web framework from scratch with just pure ruby, and we'll go build up pretty much basic versions like we did here in the beginning of this episode, rebuilding all of that functionalities, so you can see how all of this stuff works, and realize like rails and these web frameworks are really not that complicated, they just do a lot of stuff, and they've already done it for you. If you're interested in those videos, leave a comment below, so I can kind of gauge how much interested you guys are. I think it would be really fun to do, and I think it would be really really helpful for anybody looking to get a really good understanding of how rails works, and for me, understanding all those underlying tools really change the way I look at rails, because I go dive into rails source code anytime I have a question about like: Well, I'm trying to convert this virtual attribute to date, why don't I just dive into the rails code and see how they do it, and then access that code or try to do a copy of that or simpler version of it or whatever. That's what we were doing last night and it really ends up saving you a lot of time. If you're interested in that and want to remove sort of the scary feeling of diving into rails deeper or gems or any of that stuff, let me know in the comments below and what you would like to see as well. I hope you enjoyed this episode, I have a little bit of a sore throat, so I hope it wasn't too bad, but then I will talk to you next week. Peace

Loading...

Subscribe to the newsletter

Join 18,000+ developers who get early access to new screencasts, articles, guides, updates, and more.

By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

More of a social being? We're also on Twitter and YouTube.