eXpansion2 - Abandoning Doctrine & Going with Propel
Among the many things I try and manage at home one of my favorites ongoing projects is eXpansion2.
For those that doesen't know what it's about; it's a server controller for the Maniaplanet(Tracknmania) game. It connects to the games dedicated server and adds interfaces to manage the server as well as new features such as records. The controller runs as a deamon on the server.
eXpansion is a project we started a while back, been 5 years or maybe even more. The first version was based on a controller made by the game's developpers. The last version of the game has broken our controller and we decided it was time to start from scrath(see other blog posts with why).
The new version of the controller is based on symfony(yes yes strange) for it's dependency injection mechanism as well as configuration system. And of course we started to use Doctrine for our database as it was in the package.
At first this allowed us to develop very quickly a record system. And it worked very well. We loaded the records at the beginning of the map. Saved(persisted) them and detached them from doctrine at the end of the map. There was no memory issues, or performance issues.
We then did a new entity, the player entity. Basically for each player that connects to the server we save some data in the database such as his nickname. This also worked great we loaded player when they connected and detached them when they disconnected doing necessery persists in between.
Things got out of hand when we connected the records with the players. So we simply added a relationship between records & players. And all hell broke loose... If you know doctrine you could see this coming from far away. But well I had done a stupid supossitions about detached entities. First let's see what happened.
What went wrong
Let's take a very simple exemple :
- A player connects, an entity is created as saved in the database. it's kept in a attached state.
- He drives a new record. Player entity is attached to the record entity.
- He disconnects before the end of the map. Entity is saved again and detached from doctrine.
- Maps end, we try and save the record. And we of course get an error. About the player being detached.
My assumption was that as the PlayerEntity had an Id, and so doctrine would do a simple update. But no, it can only work with attached elements.
The first solution was to merge back the player entity to attach it to doctrine again. But this means to save 100 records I would need to possibly merge 100 players; so 100 select queries and quite heavy php treatment as it compares both versions.
The second solution is not to detach the players as long as they are used. But how can I know that they are used? I need a generic system not just something that works with LocalRecords. To do this I would need to make something reltively complicated with a hight possibility of a memory leak.
The problem is not doctrine.
Spending some time reading comments of on Doctrine made me realise was never designed for this use case. It can do mass actions in long scripts that has a perfect control of when data is needed but in our case it was not possible.
Even thought I think it would have been a nice addon to be able to force merge entities to reattach them into doctrine. I had to look somewhere else.
Propel - my savior?
The first thing I looked into was propel, for a 2 simple reasons :
- I had in mind testing it like forever.
- It reminds me of the ORM built in RBSChange and I think that one was great.
You can't really compare doctrine & Propel as they are built on different principles. Doctrine has a data mapper aproach with the everything related to all the data stored in ine place. Propel is based on Active Record. With each "object" therefore aware of it's own condition.
In doctrine to save an entity you need to
So the object is aware of the database and has a notion of what is going on. This means there is no central pool of data that needs to be managed. This of course comes with it's own list of drawbacks. But for me it brings a huge simplification, unsetting an object will simply free the memory.
The issue with Active Record.
Let's see this with an exemple, if we take again an exemple with record.
- New map starts, records are loaded.
- Player with record connects. Player is loaded.
- Player object is modified for X reasons.
- Player object is saved.
At this point if you do $record->getPlayer() you will not see the last changes made. That's because you have the data loaded from the database when the map started. So basically you have multiple objects for the same player.
Well if you know propel you are proably grinding your teeth about everyhing I just said. Yes Propel has a dataPool that centralized the data. But I can ignore it and clear all the data in it without affecting the workings of propel.
This issue is no pb in my use case as only the PlayerBundle is suppose to modify the Player data, the RecordsBundle will access the data but never should it modify it.
Wrapping things up
I personnaly am very split in the 2 methodologies used by this 2 orm's. I like the way doctrine does it as it very much transparent; and most importantly my entities are not clogged with methods.
Propel is easier to use, but heavily relies upon static classes and make it hard to test the objects. I am obliged to create a QueryHelper service that actually call the save method of the object because if not it's impossible to do unit tests without a database behind.
For eXpansion the choice at the moment is clear, time will say if it was the right choice.