DEV Community

Sebastian Spiegel
Sebastian Spiegel

Posted on • Updated on

Scope methods, has_secure_password, and other things that are too powerful for comprehension

The following reflects the experience of a new programmer using Ruby on Rails for the first time.

Anybody who says "you can never do too much" hasn't had enough experience with baking or coding.

If you've ever tried baking in a small batch when your normal recipe is meant to feed 10+, it usually doesn't go exactly according to plan the first time or so. With practice, you learn how much to divide or multiply the amounts, and there are bakers who can measure out enough for the exact number of cupcakes they intend to make without a drop of batter going to waste.

In coding, it's also easy to do too much. There's harmless if cluttered repetitious code, or extra validations just because you want to make good and sure no invalid users are traipsing down a certain path. There are the ones that end up canceling each other out.

There's also just straight-up making two different methods that do the same exact thing because you forgot you'd already made one and put it in your model and then you go and do the same thing and slide it into a helper.

Then there is just doing too much.

Here's where we go off the rails

Ok, let me see if I can backtrack and break this down.

The end goal is to display the name of the Location that has the most Monsters. A Contract includes both the location.id and the monster.id.

We start with a join table, linking contracts.location_id with locations.id, and then group them by location_id. Order that by the count of location_id (descending order), and the first one we get back is the location.id that has appeared the most number of times across all contracts!

Well, actually what we get back from running Active Record through a rails app is an array of two numbers. But we have a method in the Contract model that at least got us that far:

scope :location_with_most_monsters, -> {select(:location_id).joins(:location).group(:location_id).order("count_location_id DESC").count.first}
Enter fullscreen mode Exit fullscreen mode

This took several hours. I made a note for myself: first number = location_id, second number = # of contracts out.

I stared at this for a while, trying to poke that array into being... any type of useable information. Then I went to bed.

New day! We have an array, and one of those numbers happens to be the id number of a location. I need that location name. I can't just give my client an id number, it would be meaningless.

So let's take that first method over to the location model, try to find that location. Luckily a full night of sleep helped me remember that I could call .first on that array to get back just that first number (good thing I left that note for myself). We can call .find on an integer, and this is the integer we want!

So we have our second model:

scope :with_most_monsters, -> { find(Contract.location_with_most_monsters.first) }
Enter fullscreen mode Exit fullscreen mode

Now that we have the Location instance, we can take it over to the controller and save it to a variable for our view.

@dangertown = Location.with_most_monsters
Enter fullscreen mode Exit fullscreen mode

So close! Of course, we can't just give our client this entire instance. They don't care about the date and time this was added to the database or the id number! They need a name. There are monsters and this particular location has a lot of them. Lives could be at stake.

So in the view, we provide that name:

Location with most monsters: <%= @dangertown.name %>
Enter fullscreen mode Exit fullscreen mode

There you go! Hours of work, so much SQL, to get 4 lines of code so that a client knows which town has the most monsters.

HOURS of work. I can't actually say how many, only that when I started the sun was out and my roommate wasn't home from work, and when my headphones died and I couldn't listen to the Chez Baldwin playlist anymore, the sun was gone and my roommate was cleaning up their dishes from dinner.

Why would any sane person need to spend that much time to come out with so little code that does ONE thing?

Well, lives are at stake! Fictional lives, dealing with fictional monsters. But one day the lives might be real. At least next time it probably won't take me as many hours to figure out the logic.

Lives, and passwords, at stake

"Well", you might say "at least you didn't then go on to spend several hours coding something that you didn't need to code!"

"Oh," I might reply "but I did."

Security is important (lives might depend on that as well!), so adding a password confirmation for new users seemed like a good idea.

Ok, cool! Let's add a 'password_confirmation' field to the form, and then of course we'll need to add that to our permitted params and then--

You stop. You stop what you are doing right now because that's it. has_secure_password knows what it's doing. You do not need to go into the controller and put, idk, something like

if params[:user][:password] != params[:user][:password_confirmation]
flash[:message] = "Passwords do not match!"
render :new
else...
Enter fullscreen mode Exit fullscreen mode

And then you really don't need to spend an hour figuring what went wrong. has_secure_password does the confirmation for you! And the error message! Stop doing so much work! Take a break!

And don't even get me started on nested forms

Are you currently trying to build a nested form? And is it broken? And is it broken in new and exciting ways every time you catch one thing?

Here's the most important thing to remember: Leave. The. Controller. Action. Alone. That create method is just fine and if you start doing weird params validations in that method you're just leaving these wonderful rails methods to sit around doing nothing!

I thought I had it all figured out with 'accepts_nested_attributes_for' but then I just had to go and add validations to my data. So now we have a show-off.

In one corner, models#contracts/ accepts_nested_attributes_for :monster, which is just doing its job of letting a monster be associated with a contract. "Yes!" it says to a new contract instance, "Wonderful that you have that little box on the form, let 'em all in!"

Well, not all of them? We don't want nameless monsters, after all! (What could be scarier than a nameless monster?) Or a duplicate. So in the other corner, we set up models#monsters/ validates :name, uniqueness: true, presence: true and it proceeds to do its job of traffic control, not letting those pesky nameless and duplicate monsters into the database.

But our user can choose an existing monster for their contract, OR create a new one. So if they choose an existing monster, the field for the possible new one is left blank, and we can't have a blank monster, so that issue hits the validation and now contracts aren't being created and monsters are running wild and we're back to lives being threatened while these two duke it out.

So before you go diving into that controller, begging it to please meditate, there's one more little bit you should consider. (And leave the controller out of it, this fight is between models only.)

reject_if: is your mediator in this situation. Link him up and you get something along the lines of

accepts_nested_attributes_for :monster, reject_if: proc { |attributes| attributes['name'].blank? } 
Enter fullscreen mode Exit fullscreen mode

Peace reigns. Contracts of all kinds are welcome in the database. Monsters are kept in check.

And now a train metaphor

I was told Ruby on Rails was powerful, but that didn't stop me from having a dozen of those head explosion moments every day working with it. And now with all these new and exciting things available for me, I find myself asking "How do you know you're done?" and I suddenly have a lot more sympathies for people working on actual live sites.

At least in a perpetual stage of development, I can keep working on the features of my app at my own pace, laying down tracks for a train that might never come, polishing them to a shine.

I can, in a state of questionable sanity, rip up my entire database and add in three new models with only two days until the project is due.

And then if the new tracks I put down are a bit crooked, I can just put down some nice blankets and pretend that part doesn't exist for a while.

I assume that's something you could do with a train track. If it was your train track and you controlled the trains on it, at least. Probably not a good idea to do on a live, public train track. Might end up putting some lives in jeopardy.

Top comments (0)