Is ORM abstraction a pipe dream?October 21, 2013
Abstraction layers in your projects allow for extreme flexibility. That's a given. Define a QueueInterface and create implementations for Beanstalkd, Amazon SQS, and IronMQ. Swap them out without touching anything but your IoC container. That's awesome.
I was recently introduced to the repository pattern, a type of abstraction and organizational technique. The idea being, create a repository for each of your models to retrieve and persist to and from. A supposed benefit of the repository pattern is the ability to abstract your ORM and create different implemenations for Eloquent, Doctrine, Propel, etc. This abstraction intrigued me. I set off to put this idea into practice and see what it took. Here are my findings.
My goal for this project was to abstract my ORM. Pretty straightforward, right? Just take the repository pattern to the extreme. Way easier said than done.
Time to go to work
I decided my first task was to create a UserRepository interface. An interface that had method stubs like
Next up was to create my first implementation. Because Laravel is my framework, it made sense to implement the Eloquent ORM first. That was pretty easy, but then I realized something...my EloquentUserRepository is going to return Eloquent models. Okay...need to create a UserInterface.
Looks like I need to abstract Eloquent's sexy dynamic setter/getter action. I created my UserInterface with
Persisting for ORM X
At this point I started thinking I should peek at some other ORMs to see how they do things. Doctrine was in my line of sights. As soon as I learned about Doctrine's EntityManager I knew architecting this thing was going to be difficult.
In Doctrine, you wouldn't typically call
save() on your models. You use the EntityManager to
flush() the models. This wraps the save in a SQL transaction for you. I like that a lot. It means transactions are handled for you when you do something like the following.
$user = new User(); $user->setName('David'); $profile = new UserProfile(); $profile->setLocation('Dallas'); $profile->setOccupation('Software Developer'); // relate this profile to the user $profile->setUser($user); $em = $this->getDoctrine()->getManager(); $em->persist($user); $em->persist($profile); $em->flush();
I want to use transactions with Eloquent, so it looks like I need to add a
save(UserInterface $user) method to the UserRepository interface. I'll just call
save() on the Eloquent model inside of the EloquentUserRepository's
save(UserInterface $user) method.
Hmm...what about relationships?
After looking at Doctrine, I realized I needed to create different method stubs somewhere for relating and un-relating models.
I wonder how Propel handles relationships. Hmm...looks like you set related models on each other and then
save() it, which cascades the saves.
//Propel relationship saving $author = new Author(); $author->setFirstName("Leo"); $author->setLastName("Tolstoy"); // no need to save the author yet $publisher = new Publisher(); $publisher->setName("Viking Press"); // no need to save the publisher yet $book = new Book(); $book->setTitle("War & Peace"); $book->setIsbn("0140444173"); $book->setPublisher($publisher); $book->setAuthor($author); $book->save(); // saves all 3 objects!
About that...do I create a UserRelationship interface? Do I add methods to the UserInterface? How should I go about handling the cascading saves by Propel versus the immediate saves done by Eloquent versus the flushing done by Doctrine? Uhhh...I'll just not worry about that for now. Validation is what I need next!
I like the idea of validating data for models before I stick the data inside the model. In Laravel, I can just create a create a class specifically for validation and use Laravel's validation class. I'll do that and validate an array of POST data from the request.
Just to be safe, I better take a peek at how Doctrine handles validation. Umm...a bit different. Looks like I can get a handle on a validator object and pass it a model or I can create a some kind of form object from my models and validate it by passing it a Request object. Looks like a need to create interfaces for each of my form validators and have different implementations for each ORM. That sucks.
Wonder how Propel handles validation. Looks like you validate a Propel model by calling a
validate() on the model itself. Great...so Laravel validates an array of data, Doctrine a Request object, and Propel the models themselves. How do I interface that?
I think I want to be able to hook into different events and have event listeners for certain actions. Eloquent already has some Eloquent events defined for saving, updating, deleting models. That's nice but no good for me. I need generic events being fired.
I need to fire those same events in all of my UserRepository implementations. Yet another thing to keep in mind.
This is rough
Geez, I had no idea how much planning this would take. I'm not getting anywhere. Although a cool idea, this proof of concept isn't worth the time I'm spending on it.
What do you have to show for this?
Not much! :( I wish I had some cool demo where I could change a flag in the code and Eloquent would be used, change it again and Doctrine would be used, change it again and Propel would be used...but, alas, I do not.
The truth is I've spent so much time reading about how these different ORM's handle different aspects and not writing any code. After learning how different each ORM is, I've found it incredibly difficult to interface the different components to make an ORM agnostic project work.
I do not want to sound like I'm whining about something I was promised by using the repository pattern. That is not the intention of this post at all. The repository pattern most definitely has its benefits, but completely abstracting your ORM was a little too difficult for me. It was hindering me from actually producing anything.
It's not all doom and gloom for me though. This forced me to take a look at Doctrine(which I'm liking the more I see) and Propel. Learning how these different ORM's handle validation, relationships, transacations, etc has been a great experience for me. It has helped me form some opinions about what I like and don't like in an ORM, which is great. I didn't even think about that indirect benefit when I started.
As of right now, for me, I've decided that I can use the repository pattern for organizational purposes, but not ORM abstraction. I think I need to pick an ORM and stick with it to get anything done.
Overall I'm really glad I attempted this. I learned a lot. I hope this post starts a discussion on proper ORM abstraction. I would love to here your guys' thoughts on the subject.