April 8, 2015 by Daniel P. Clark

ActiveRecord vs Me… Round 1 – FIGHT!

I’ve done a fair bit of work in creating a library that integrates with ActiveRecord Objects.  I also have a pretty good grasp on mixins/inheritance.  Yet I still run into situations where trying include, extend, and techniques for monkey patching just don’t work out.

I was trying to implement a method like first_or_create on the ActiveRecord::QueryMethods::WhereChain, and/or ActiveRecord::Relation, and possibly write the appropriate delegation on ActiveRecord::Querying.  I literally spent many hours without success.  I even tried ActiveRecord::Base (which isn’t what I need) simply because I have had success with that for something else.

The way I was looking in to doing it was by first going to the Rails source code and seeing how first_or_create was implemented.  The previous modules and classes I’ve mentioned all have something to do with it.  But with lots of effort and no success I must say it was very frustrating experience.

A Working Solution

So the method I was trying to implement is called first_or_force_create.  It’s not really a method you want lying around for other people to use because it bypasses validations and could be a potential mess waiting to happen.  But I did want to implement it as a tool I could use when needed for the rare case I’ve come upon.

I implemented it by creating a lambda to be passed into, and evaluated within the context of, the WhereChain to allow the where clauses to be attributes on what’s created.

First I create a file in the lib directory first_or_force_create.rb .

FirstOrForceCreate = ->(attributes = nil, &block) {
  first || (
    obj = new(attributes, &block)
    obj.save(validate: false)
    obj
  )
}

I may move it into a sub-folder, maybe call it hack or monkey_smashes.  Also I may scope it within a module to not pollute the main name space with, potentially many, lambda methods.

Use Case: I needed a field to be nil that would always be validated as not nit otherwise.  I have a messaging system using the ActsAsMessageable gem which will only send messages between ActiveRecord objects of the same Class and record type.  So the User model will have many Identities (has_many :identities) and Identity (belongs_to :user; validates_presence_of :user_id) is the ActiveRecord object that we add ActsAsMessageable to.  So Identities may now send messages between each other and they require Identities for both to and from.  But I want to integrate incoming SMS messages into the messages.  So I need to implement an Identity that doesn’t have a User as an owner.  It will be the “Incoming SMS” Identity.

Identity.where(name: "Incoming SMS").instance_exec &FirstOrForceCreate

This can now be the way I create the identity to message with and use it with my incoming SMS API. Notice there’s no user_id: parameter.  This command creates a new Identity record with the name field as “Incoming SMS” and saves it without validation.

What’s important here is the way I’m passing the lambda in.  You see that I’ve written instance_exec with that ampersand (&) in front of the lambda method?  The instance_exec method will permit arguments to be passed (should you need to); instance_eval will not.  Passing a lambda with the ampersand both calls to_proc (the & part) on the lambda and the instance_exec uses the content of the lambda as the block (Proc) to execute.

You cannot use .send to send the lambda in because you would need to execute the lambda with one of its .call methods.  And when you perform a .call in the context of .send it evaluates the lambda outside the Object before actually sending it in.  In this case it will give a ‘NoMethodError `first’ for main:’ because lambda’s version of self is evaluated by the external context in which it is called.  So you will need to evaluate the lambda inside the Object you wish to use it on with something like instance_exec &MyLambda .

Summary

Making a generally bad idea method to be used in an unconventional way may be a good strategy to help prevent amateurs from using it.  You wouldn’t want your checks and balances in your code bypassed by everyone, that defeats the purpose.  So having a stash of lambdas at hand could be nice for one-offs.  Just be mindful that if you leave an instance of it being used in the code base some one might try to mimic it. *frown* 🙁

Also if you know of a better way to do any of what I’ve mentioned above I’d love to hear it!  Whether it’s adding a method to work in the same way as first_or_create that works on a WhereChain, or it’s implementing ActsAsMessageable to work with other Objects and interfaces like incoming SMS.

There’s nothing quite as frustrating as knowing how things generally work and spending hours without success.  So I wanted to at least spare some some potential headaches by providing something that works.  If you want to implement something on ActiveRecord instances and scopes that’s easy, you can see how I’ve done it on my gem PolyBelongsTo with ActiveRecord::Base.  Working on the WhereChain is a whole other can of beans.  It may be easy, but not without the proper understanding of it; which I don’t have as of yet.

Passing lambdas as blocks of code to be evaluated inside some other context is hugely powerful.  They’re very much worth looking into.  Even if you know them, there’s ‘always’ more to learn.

Hopefully you found this both helpful and inspirational.  Anything to help the mind thrive.  Feel free to inform me on anything I’m mistaken about; I’d love to know!  Please feel free to comment, share, subscribe to my RSS Feed, and follow me on twitter @6ftdan!

God Bless!
-Daniel P. Clark

Image by Sumeet Moghe via the Creative Commons Attribution-NonCommercial-ShareAlike 2.0 Generic License

#activerecord#instance_exec#lambda#rails#ruby on rails#WhereChain

Comments

  1. nil2
    April 9, 2015 - 8:08 am

    Does first_or_force_create do something that you need differently than ActiveRecord’s built-in find_or_create_by? That you wanted to skip the validations? http://apidock.com/rails/v4.1.8/ActiveRecord/Relation/find_or_create_by

    • Daniel P. Clark
      April 9, 2015 - 1:53 pm

      That is the behavior I would like. Except that it calls create and create persists a record to the DB with out taking any options for doing so without validation. As you can see I targeted the WhereChain so that I could put the where clauses before the command.

      If I were to choose to write a method like first_or_force_create_by then I could include it into ActiveRecord::Base and that would be a working solution, and would have save me a lot of time and effort.

      I tend to get stubborn in my programming sessions when I try to do something one way. For some reason I just have been forgetting that there’s more than one way to skin a (fictitious) cat.

      • nil2
        April 9, 2015 - 3:29 pm

        You can put the where clauses in the chain before the already-existing-in-AR `find_or_create_by` too.

        I don’t know if you can make `find_or_create_by ` skip validations. That does seem like a special case.

        Anyhow, if it works for you and does something `find_or_create_by` doesn’t do, I guess that’s cool. I suspect for most people the existing `find_or_create_by` will work fine.

        And I’m not sure if you’re saying you intentionally _wanted_ to give it a really confusing API where you have to call it like `where(etc).instance_exec &FirstOrForceCreate`. It sounds like maybe you actually preferred that to a more normal API? I guess that’s cool. I wouldn’t.

        • Daniel P. Clark
          April 9, 2015 - 9:02 pm

          Originally I was only thinking of mimicking first_or_create and writing a method just as easily accessible. But after failing to do so, and with additional thoughts on it, I thought of a context of having other developers see the method and I thought that it would be best if only used by those who knew what they were doing. So the “really confusing API” you refer to was an afterthought that I believe is a good deterrent for some.

Leave a Reply

Your email address will not be published / Required fields are marked *