tag:www.rhnh.net,2008:/datamapper Datamapper - Xavier Shay's Blog 2011-11-29T04:24:34Z Enki Xavier Shay notreal@rhnh.net tag:www.rhnh.net,2008:Post/857 2011-11-29T04:39:00Z 2011-11-29T04:24:34Z DataMapper Retrospective <p>I introduced <a href="http://datamapper.org/">DataMapper</a> on my last two major projects. As those projects matured after I had left, they both migrated to a different <span class="caps">ORM</span>. That deserves a retrospective, I think. As I&#8217;ve left both projects, I don&#8217;t have the insider level of detail on the decision to abandon DataMapper, but developers from both projects kindly provided background for this blog post.</p> <h2>Project A</h2> <p>Web application and a batch processing component built on top of a legacy Oracle database.</p> <h3>Good</h3> <ul> <li>Field mappings, nice ruby names and able to ignore fields we didn&#8217;t care about.</li> </ul> <h3>Bad</h3> <ul> <li>Had to roll our own locking and time zone integration.</li> <li>Not great for batch processing (trying to write <span class="caps">SQL</span> through DM abstraction.)</li> </ul> <p>It turned out this project required a lot more batch processing than we anticipated, which DataMapper does not shine at. It was migrated to <a href="http://sequel.rubyforge.org/">Sequel</a> which provides a far better abstraction for working closer to <span class="caps">SQL</span>.</p> <h2>Project B</h2> <p>A fairly typical Rails 3 application. A couple of tens of thousands of lines of code.</p> <h3>Good</h3> <ul> <li>No migrations (pre-release).</li> <li>Foreign keys, composite primary keys.</li> <li>Auto-validations.</li> </ul> <h3>Bad</h3> <ul> <li>Auto-validations with nested attributes was uncharted territory (needed bug fixes).</li> <li>Performance on large object graphs was unusable for page rendering (close to two seconds for our home page, which admittedly had a stupid amount of stuff on it).</li> <li>Performance was suboptimal (though passable) on smaller pages.</li> <li>Tracing through what his happening across multiple gems (particularly around transactions) was tricky.</li> <li>The maintenance/interactions of all the various gems was problematic (e.g. gems X,Y work with 1.9.3 but Z doesn&#8217;t yet).</li> <li>Inability to easily &#8220;break the abstraction&#8221; when <span class="caps">SQL</span> was required.</li> </ul> <p>The performance issues were clear in our code base, but eluded much effort to reduce them down to smaller reproducible problems. The best quick win I found was ~15% by disabling assertions, but I suspect that given the large scope of the problem DataMapper is trying to solve there may not be any approachable way of tackling the issue (would love to be proven wrong!)</p> <p>We ran into obvious integration bugs (apologies for not having kept a concrete list), a symptom of a library not widely used. As a commiter on the project this wasn&#8217;t an issue, since they were easily fixed and moved past (the DataMapper code base is really nice to work on), but having a commiter on your team isn&#8217;t a tenable strategy.</p> <p>DataMapper takes an all-ruby-all-the-time approach, which means things get tricky when the abstraction leaks. Much of the <span class="caps">SQL</span> generation is hidden in private methods. Compare some code to create a composable full text search query:</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt>7<tt> </tt>8<tt> </tt>9<tt> </tt><strong>10</strong><tt> </tt>11<tt> </tt>12<tt> </tt>13<tt> </tt>14<tt> </tt>15<tt> </tt>16<tt> </tt>17<tt> </tt>18<tt> </tt>19<tt> </tt><strong>20</strong><tt> </tt>21<tt> </tt>22<tt> </tt>23<tt> </tt>24<tt> </tt>25<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="r">def</span> <span class="pc">self</span>.<span class="fu">search</span>(keywords, options = {})<tt> </tt> options = {<tt> </tt> <span class="ke">conditions</span>: [<span class="s"><span class="dl">&quot;</span><span class="k">true</span><span class="dl">&quot;</span></span>]<tt> </tt> }.merge(options)<tt> </tt><tt> </tt> current_query = query.merge(options)<tt> </tt><tt> </tt> a = repository.adapter<tt> </tt> columns_sql = a.send(<span class="sy">:columns_statement</span>, current_query.fields, <span class="pc">false</span>)<tt> </tt> conditions = a.send(<span class="sy">:conditions_statement</span>, current_query.conditions, <span class="pc">false</span>)<tt> </tt> order_sql = a.send(<span class="sy">:order_statement</span>, current_query.order, <span class="pc">false</span>)<tt> </tt> limit_sql = current_query.limit || <span class="i">50</span><tt> </tt> conditions_sql, conditions_values = *conditions<tt> </tt><tt> </tt> bind_values = [keywords] + conditions_values<tt> </tt><tt> </tt> find_by_sql([<span class="s"><span class="dl">&lt;&lt;-SQL</span></span>, *bind_values])<span class="s"><span class="k"><tt> </tt> SELECT </span><span class="il"><span class="idl">#{</span>columns_sql<span class="idl">}</span></span><span class="k">, ts_rank_cd(search_vector, query) AS rank<tt> </tt> FROM things<tt> </tt> CROSS JOIN plainto_tsquery(?) query<tt> </tt> WHERE </span><span class="il"><span class="idl">#{</span>conditions_sql<span class="idl">}</span></span><span class="k"> AND (query @@ search_vector)<tt> </tt> ORDER BY rank DESC, </span><span class="il"><span class="idl">#{</span>order_sql<span class="idl">}</span></span><span class="k"><tt> </tt> LIMIT </span><span class="il"><span class="idl">#{</span>limit_sql<span class="idl">}</span></span><span class="dl"><tt> </tt> SQL</span></span><tt> </tt><span class="r">end</span><tt> </tt></pre></td> </tr></table> <p>To the ActiveRecord equivalent (Sequel is similar):</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="r">def</span> <span class="pc">self</span>.<span class="fu">search</span>(keywords)<tt> </tt> select(<span class="s"><span class="dl">&quot;</span><span class="k">things.*, ts_rank_cd(search_vector, query) AS rank</span><span class="dl">&quot;</span></span>)<tt> </tt> .joins(sanitize_sql_array([<span class="s"><span class="dl">&quot;</span><span class="k">CROSS JOIN plainto_tsquery(?) query</span><span class="dl">&quot;</span></span>, keywords]))<tt> </tt> .where(<span class="s"><span class="dl">&quot;</span><span class="k">query @@ search_vector</span><span class="dl">&quot;</span></span>)<tt> </tt> .order(<span class="s"><span class="dl">&quot;</span><span class="k">rank DESC</span><span class="dl">&quot;</span></span>)<tt> </tt><span class="r">end</span><tt> </tt></pre></td> </tr></table> <p>Switching to ActiveRecord took a week of all hands (~4) on deck, plus another week alongside other feature work to get it stable. From beginning to in production was two weeks. The end result was a drop in response time (the deploy is pretty blatant in the graph below), start up time, plus 3K less lines of code (a lot of custom code for dropping down to <span class="caps">SQL</span> was able to be removed).</p> <p><img src="http://a.yfrog.com/img739/7449/4h5.png" alt="" /></p> <h2>Do differently</h2> <p>Ultimately, DataMapper provides an abstraction that I just don&#8217;t need, and even if I did it hasn&#8217;t had its tires kicked sufficiently that a team can use it without having to delve down to the internals. The applications I find myself writing are about data, and the store in which that data lives is vitally important to the application. Abstracting away those details seems to be heading in the wrong direction for writing simple applications. As an intellectual achievement in its own right I really dig DataMapper, but it is too complicated a component to justify using inside other applications.</p> <p>Rich Hickey&#8217;s talk <a href="http://www.infoq.com/presentations/Simple-Made-Easy">Simple Made Easy</a> has been rattling around my head a lot.</p> <p>Nowadays I&#8217;m back to ActiveRecord for team conformance. It&#8217;s more work to keep on top of foreign keys and the like, but overall it does the job. It&#8217;s still too complicated, but has the non-trivial benefit of being used by <strong>lots</strong> of people. This is my responsible choice at the moment.</p> <p>On my own projects I first reach for Sequel. It supports all the nice database features I want to use, while providing a thin layer over <span class="caps">SQL</span>. In other words, I don&#8217;t have to worry about the abstraction leaking because the abstraction is still <span class="caps">SQL</span>, just expressed in ruby (which is a huge win for composeability that you don&#8217;t get with raw <span class="caps">SQL</span>). While it does have &#8220;<span class="caps">ORM</span>&#8221; features, it feels more like the most convenient way of accessing my database rather than an abstraction layer. It&#8217;s actively maintained and the only bug I have found was something that Rails broke, and a patch was already available. There are no open issues in the bug tracker. My experiences have been overwhelmingly positive. I haven&#8217;t built anything big enough with it yet to have confidence using it on a team project though.</p> <p>I still have a soft spot in my heart for DataMapper, I just don&#8217;t see anywhere for me to use it anymore.</p> tag:www.rhnh.net,2008:Post/844 2011-02-25T04:30:52Z 2011-02-25T04:30:52Z PostgreSQL 9 and ruby full text search tricks <p>I have just released an introduction to PostgreSQL screencast, published through PeepCode. It is over an hour long and covers a large number of juicy topics:</p> <ul> <li>Setup full text search</li> <li>Optimize search with triggers and indexes</li> <li>Use Postgres with Ruby on Rails 3</li> <li>Optimize indexes by including only the rows that you need</li> <li>Use database standards for more reliable queries</li> <li>Write powerful reports in only a few lines of code</li> <li>Convert an existing MySQL application to use Postgres</li> </ul> <p>It&#8217;s a steal at only $12. You can <a href="http://peepcode.com/products/postgresql">buy it over at PeepCode</a>.</p> <p>In it, I introduce full text search in postgres, and use a trigger to keep a search vector up to date. I&#8217;m not going to cover that here, but the point I get to is:</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="r">CREATE</span> <span class="r">TRIGGER</span> posts_search_vector_refresh <tt> </tt> <span class="r">BEFORE</span> <span class="r">INSERT</span> <span class="r">OR</span> <span class="r">UPDATE</span> <span class="r">ON</span> posts <tt> </tt>FOR EACH ROW EXECUTE PROCEDURE<tt> </tt> tsvector_update_trigger(search_vector, <span class="s"><span class="dl">'</span><span class="k">pg_catalog.english</span><span class="dl">'</span></span>, body, title);<tt> </tt></pre></td> </tr></table> <p>That is good for simple models, but what if you want to index child models as well? For instance, we want to include comment authors in the search index. I rolled up my sleeves an came up with this:</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt>7<tt> </tt>8<tt> </tt>9<tt> </tt><strong>10</strong><tt> </tt>11<tt> </tt>12<tt> </tt>13<tt> </tt>14<tt> </tt>15<tt> </tt>16<tt> </tt>17<tt> </tt>18<tt> </tt>19<tt> </tt><strong>20</strong><tt> </tt>21<tt> </tt>22<tt> </tt>23<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="r">CREATE</span> <span class="r">OR</span> <span class="r">REPLACE</span> FUNCTION search_trigger() RETURNS <span class="r">trigger</span> <span class="r">AS</span> <span class="er">$</span><span class="er">$</span><tt> </tt>DECLARE<tt> </tt> search <span class="pt">TEXT</span>;<tt> </tt> child_search <span class="pt">TEXT</span>;<tt> </tt><span class="r">begin</span><tt> </tt> <span class="r">SELECT</span> string_agg(author_name, <span class="s"><span class="dl">'</span><span class="k"> </span><span class="dl">'</span></span>) <span class="r">INTO</span> child_search<tt> </tt> <span class="r">FROM</span> comments<tt> </tt> <span class="r">WHERE</span> post_id = new.id;<tt> </tt><tt> </tt> search <span class="er">:</span>= <span class="s"><span class="dl">'</span><span class="dl">'</span></span>;<tt> </tt> search <span class="er">:</span>= search || <span class="s"><span class="dl">'</span><span class="k"> </span><span class="dl">'</span></span> || coalesce(new.title);<tt> </tt> search <span class="er">:</span>= search || <span class="s"><span class="dl">'</span><span class="k"> </span><span class="dl">'</span></span> || coalesce(new.body);<tt> </tt> search <span class="er">:</span>= search || <span class="s"><span class="dl">'</span><span class="k"> </span><span class="dl">'</span></span> child_search;<tt> </tt><tt> </tt> new.search_index <span class="er">:</span>= to_tsvector(search); <tt> </tt> return new;<tt> </tt><span class="r">end</span><tt> </tt><span class="er">$</span><span class="er">$</span> LANGUAGE plpgsql;<tt> </tt><tt> </tt><span class="r">CREATE</span> <span class="r">TRIGGER</span> posts_search_vector_refresh <tt> </tt> <span class="r">BEFORE</span> <span class="r">INSERT</span> <span class="r">OR</span> <span class="r">UPDATE</span> <span class="r">ON</span> posts<tt> </tt>FOR EACH ROW EXECUTE PROCEDURE<tt> </tt> search_trigger();<tt> </tt></pre></td> </tr></table> <p>Getting a bit ugly eh. It might be nice to move that logic back into ruby land, but we have the problem that we need to call a database function to convert our search document into the correct data-type. In this case, a quick work around is to store a <code>search_document</code> in a text field on the model, then use a trigger to only index that field into our <code>search_vector</code> field. The <code>search_document</code> field can then easily be set from your <span class="caps">ORM</span>.</p> <p>Of course, any self-respecting rubyist should hide all this complexity behind a neat interface. I have come up with one using DataMapper that automatically adds the required triggers and indexes via auto-migrations. You use it thusly:</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt>7<tt> </tt>8<tt> </tt>9<tt> </tt><strong>10</strong><tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="r">class</span> <span class="cl">Post</span><tt> </tt> include <span class="co">DataMapper</span>::<span class="co">Resource</span><tt> </tt> include <span class="co">Searchable</span><tt> </tt><tt> </tt> property <span class="sy">:id</span>, <span class="co">Serial</span><tt> </tt> property <span class="sy">:title</span>, <span class="co">String</span><tt> </tt> property <span class="sy">:body</span>, <span class="co">Text</span><tt> </tt><tt> </tt> searchable <span class="sy">:title</span>, <span class="sy">:body</span> <span class="c"># Provides Post.search('keyword')</span><tt> </tt><span class="r">end</span><tt> </tt></pre></td> </tr></table> <p>You can find the <a href="https://github.com/xaviershay/sandbox/blob/master/misc/searchable.rb">Searchable module code over on github</a>. In it you can also find a fugly proof-of-concept for a <span class="caps">DSL</span> that generates the above <span class="caps">SQL</span> for indexing child models using DataMapper&#8217;s rich property model. It worked, but I&#8217;m not using it in any production code so I can hardly recommend it. Maybe you want to have a play though.</p> tag:www.rhnh.net,2008:Post/841 2010-12-01T01:01:58Z 2010-12-01T01:01:58Z Ordering by a field in a join model with DataMapper <p>The public interface for datamapper 1.0.3 does not support ordering by a column in a joined model on a query. The core of datamapper does support this though, so we can use some hacks to make it work, as the following code demonstrates.</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt>7<tt> </tt>8<tt> </tt>9<tt> </tt><strong>10</strong><tt> </tt>11<tt> </tt>12<tt> </tt>13<tt> </tt>14<tt> </tt>15<tt> </tt>16<tt> </tt>17<tt> </tt>18<tt> </tt>19<tt> </tt><strong>20</strong><tt> </tt>21<tt> </tt>22<tt> </tt>23<tt> </tt>24<tt> </tt>25<tt> </tt>26<tt> </tt>27<tt> </tt>28<tt> </tt>29<tt> </tt><strong>30</strong><tt> </tt>31<tt> </tt>32<tt> </tt>33<tt> </tt>34<tt> </tt>35<tt> </tt>36<tt> </tt>37<tt> </tt>38<tt> </tt>39<tt> </tt><strong>40</strong><tt> </tt>41<tt> </tt>42<tt> </tt>43<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }">require <span class="s"><span class="dl">'</span><span class="k">rubygems</span><span class="dl">'</span></span><tt> </tt>require <span class="s"><span class="dl">'</span><span class="k">dm-core</span><span class="dl">'</span></span><tt> </tt>require <span class="s"><span class="dl">'</span><span class="k">dm-migrations</span><span class="dl">'</span></span><tt> </tt><tt> </tt><span class="co">DataMapper</span>::<span class="co">Logger</span>.new(<span class="gv">$stdout</span>, <span class="sy">:debug</span>)<tt> </tt><span class="co">DataMapper</span>.setup(<span class="sy">:default</span>, <span class="s"><span class="dl">'</span><span class="k">postgres://localhost/test</span><span class="dl">'</span></span>) <span class="c"># createdb test</span><tt> </tt><tt> </tt><span class="r">class</span> <span class="cl">User</span><tt> </tt> include <span class="co">DataMapper</span>::<span class="co">Resource</span><tt> </tt><tt> </tt> property <span class="sy">:id</span>, <span class="co">Serial</span><tt> </tt><tt> </tt> has <span class="i">1</span>, <span class="sy">:user_profile</span><tt> </tt><tt> </tt> <span class="r">def</span> <span class="pc">self</span>.ranked<tt> </tt> order = <span class="co">DataMapper</span>::<span class="co">Query</span>::<span class="co">Direction</span>.new(user_profile.ranking, <span class="sy">:desc</span>) <tt> </tt> query = all.query <span class="c"># Access a blank query object for us to manipulate</span><tt> </tt> query.instance_variable_set(<span class="s"><span class="dl">&quot;</span><span class="k">@order</span><span class="dl">&quot;</span></span>, [order])<tt> </tt><tt> </tt> <span class="c"># Force the user_profile model to be joined into the query</span><tt> </tt> query.instance_variable_set(<span class="s"><span class="dl">&quot;</span><span class="k">@links</span><span class="dl">&quot;</span></span>, [relationships[<span class="s"><span class="dl">'</span><span class="k">user_profile</span><span class="dl">'</span></span>].inverse])<tt> </tt><tt> </tt> all(query) <span class="c"># Create a new collection with the modified query</span><tt> </tt> <span class="r">end</span><tt> </tt><span class="r">end</span><tt> </tt><tt> </tt><span class="r">class</span> <span class="cl">UserProfile</span><tt> </tt> include <span class="co">DataMapper</span>::<span class="co">Resource</span><tt> </tt><tt> </tt> property <span class="sy">:user_id</span>, <span class="co">Integer</span>, <span class="sy">:key</span> =&gt; <span class="pc">true</span><tt> </tt> property <span class="sy">:ranking</span>, <span class="co">Integer</span>, <span class="sy">:default</span> =&gt; <span class="i">0</span><tt> </tt><tt> </tt> belongs_to <span class="sy">:user</span><tt> </tt><span class="r">end</span><tt> </tt><tt> </tt><span class="co">DataMapper</span>.finalize<tt> </tt><span class="co">DataMapper</span>.auto_migrate!<tt> </tt><tt> </tt><span class="co">User</span>.create(<span class="sy">:user_profile</span> =&gt; <span class="co">UserProfile</span>.new(<span class="sy">:ranking</span> =&gt; <span class="i">2</span>))<tt> </tt><span class="co">User</span>.create(<span class="sy">:user_profile</span> =&gt; <span class="co">UserProfile</span>.new(<span class="sy">:ranking</span> =&gt; <span class="i">5</span>))<tt> </tt><span class="co">User</span>.create(<span class="sy">:user_profile</span> =&gt; <span class="co">UserProfile</span>.new(<span class="sy">:ranking</span> =&gt; <span class="i">3</span>))<tt> </tt><tt> </tt>puts <span class="co">User</span>.ranked.map {|x| x.user_profile.ranking }.inspect<tt> </tt></pre></td> </tr></table> tag:www.rhnh.net,2008:Post/837 2010-10-06T23:31:02Z 2010-10-06T23:31:02Z Transactional before all with RSpec and DataMapper <p>By default, <code>before(:all)</code> in rspec executes <em>outside</em> of any transaction, meaning that you can&#8217;t really use it for creating objects. Normally this should go in a <code>before(:each)</code>, but for a spec with simple creation and a large number of assertions this is terribly inefficient.</p> <p>Let&#8217;s fix it!</p> <p>This code assumes you are using DataMapper, and that your database supports some form of nested transactions (at the very least faking them with savepoints &#8211; see <a href="http://rhnh.net/2010/10/06/nested-transactions-in-postgres-with-datamapper">nested transactions in postgres with datamapper</a>). It wraps each before/after <code>:all</code> and <code>:each</code> in it&#8217;s own transaction.</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt>7<tt> </tt>8<tt> </tt>9<tt> </tt><strong>10</strong><tt> </tt>11<tt> </tt>12<tt> </tt>13<tt> </tt>14<tt> </tt>15<tt> </tt>16<tt> </tt>17<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="co">RSpec</span>.configure <span class="r">do</span> |config|<tt> </tt> [<span class="sy">:all</span>, <span class="sy">:each</span>].each <span class="r">do</span> |x|<tt> </tt> config.before(x) <span class="r">do</span><tt> </tt> repository(<span class="sy">:default</span>) <span class="r">do</span> |repository|<tt> </tt> transaction = <span class="co">DataMapper</span>::<span class="co">Transaction</span>.new(repository)<tt> </tt> transaction.begin<tt> </tt> repository.adapter.push_transaction(transaction)<tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> config.after(x) <span class="r">do</span><tt> </tt> repository(<span class="sy">:default</span>).adapter.pop_transaction.rollback<tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> config.include(<span class="co">RSpecExtensions</span>::<span class="co">Set</span>)<tt> </tt><span class="r">end</span><tt> </tt></pre></td> </tr></table> <p>See that <code>RSpecExtensions::Set</code> include? That&#8217;s a version of the lovely <code>let</code> helpers that works with before(:all) setup. Props to <a href="http://twitter.com/pcreux">pcreux</a> for this:</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt>7<tt> </tt>8<tt> </tt>9<tt> </tt><strong>10</strong><tt> </tt>11<tt> </tt>12<tt> </tt>13<tt> </tt>14<tt> </tt>15<tt> </tt>16<tt> </tt>17<tt> </tt>18<tt> </tt>19<tt> </tt><strong>20</strong><tt> </tt>21<tt> </tt>22<tt> </tt>23<tt> </tt>24<tt> </tt>25<tt> </tt>26<tt> </tt>27<tt> </tt>28<tt> </tt>29<tt> </tt><strong>30</strong><tt> </tt>31<tt> </tt>32<tt> </tt>33<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="r">module</span> <span class="cl">RSpecExtensions</span><tt> </tt> <span class="r">module</span> <span class="cl">Set</span><tt> </tt><tt> </tt> <span class="r">module</span> <span class="cl">ClassMethods</span><tt> </tt> <span class="c"># Generates a method whose return value is memoized</span><tt> </tt> <span class="c"># in before(:all). Great for DB setup when combined with</span><tt> </tt> <span class="c"># transactional before alls.</span><tt> </tt> <span class="r">def</span> <span class="fu">set</span>(name, &amp;block)<tt> </tt> define_method(name) <span class="r">do</span><tt> </tt> __memoized[name] ||= instance_eval(&amp;block)<tt> </tt> <span class="r">end</span><tt> </tt> before(<span class="sy">:all</span>) { __send__(name) }<tt> </tt> before(<span class="sy">:each</span>) <span class="r">do</span><tt> </tt> __send__(name).tap <span class="r">do</span> |obj|<tt> </tt> obj.reload <span class="r">if</span> obj.respond_to?(<span class="sy">:reload</span>)<tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="r">module</span> <span class="cl">InstanceMethods</span><tt> </tt> <span class="r">def</span> <span class="fu">__memoized</span> <span class="c"># :nodoc:</span><tt> </tt> <span class="iv">@__memoized</span> ||= {}<tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="r">def</span> <span class="pc">self</span>.included(mod) <span class="c"># :nodoc:</span><tt> </tt> mod.extend <span class="co">ClassMethods</span><tt> </tt> mod.__send__ <span class="sy">:include</span>, <span class="co">InstanceMethods</span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="r">end</span><tt> </tt><span class="r">end</span><tt> </tt></pre></td> </tr></table> <p>Fast specs make me a happy man.</p> tag:www.rhnh.net,2008:Post/836 2010-10-06T04:34:00Z 2010-10-06T02:34:07Z Nested Transactions in Postgres with DataMapper <p>Hacks to get nested transactions support for Postgres in DataMapper. Not extensively tested, more a proof of concept. It re-opens the existing <code>Transaction</code> class to add a check for whether we need a nested transaction or not, and adds a new <code>NestedTransaction</code> transaction primitive that issues savepoint commands rather than begin/commit.</p> <p>I put this code in a Rails initializer.</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt>7<tt> </tt>8<tt> </tt>9<tt> </tt><strong>10</strong><tt> </tt>11<tt> </tt>12<tt> </tt>13<tt> </tt>14<tt> </tt>15<tt> </tt>16<tt> </tt>17<tt> </tt>18<tt> </tt>19<tt> </tt><strong>20</strong><tt> </tt>21<tt> </tt>22<tt> </tt>23<tt> </tt>24<tt> </tt>25<tt> </tt>26<tt> </tt>27<tt> </tt>28<tt> </tt>29<tt> </tt><strong>30</strong><tt> </tt>31<tt> </tt>32<tt> </tt>33<tt> </tt>34<tt> </tt>35<tt> </tt>36<tt> </tt>37<tt> </tt>38<tt> </tt>39<tt> </tt><strong>40</strong><tt> </tt>41<tt> </tt>42<tt> </tt>43<tt> </tt>44<tt> </tt>45<tt> </tt>46<tt> </tt>47<tt> </tt>48<tt> </tt>49<tt> </tt><strong>50</strong><tt> </tt>51<tt> </tt>52<tt> </tt>53<tt> </tt>54<tt> </tt>55<tt> </tt>56<tt> </tt>57<tt> </tt>58<tt> </tt>59<tt> </tt><strong>60</strong><tt> </tt>61<tt> </tt>62<tt> </tt>63<tt> </tt>64<tt> </tt>65<tt> </tt>66<tt> </tt>67<tt> </tt>68<tt> </tt>69<tt> </tt><strong>70</strong><tt> </tt>71<tt> </tt>72<tt> </tt>73<tt> </tt>74<tt> </tt>75<tt> </tt>76<tt> </tt>77<tt> </tt>78<tt> </tt>79<tt> </tt><strong>80</strong><tt> </tt>81<tt> </tt>82<tt> </tt>83<tt> </tt>84<tt> </tt>85<tt> </tt>86<tt> </tt>87<tt> </tt>88<tt> </tt>89<tt> </tt><strong>90</strong><tt> </tt>91<tt> </tt>92<tt> </tt>93<tt> </tt>94<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="c"># Hacks to get nested transactions in Postgres</span><tt> </tt><span class="c"># Not extensively tested, more a proof of concept</span><tt> </tt><span class="c">#</span><tt> </tt><span class="c"># It re-opens the existing Transaction class to add a check for whether</span><tt> </tt><span class="c"># we need a nested transaction or not, and adds a new NestedTransaction</span><tt> </tt><span class="c"># transaction primitive that issues savepoint commands rather than begin/commit.</span><tt> </tt><tt> </tt><span class="r">module</span> <span class="cl">DataMapper</span><tt> </tt> <span class="r">module</span> <span class="cl">Resource</span><tt> </tt> <span class="r">def</span> <span class="fu">transaction</span>(&amp;block)<tt> </tt> <span class="pc">self</span>.class.transaction(&amp;block)<tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="r">class</span> <span class="cl">Transaction</span><tt> </tt> <span class="c"># Overridden to allow nested transactions</span><tt> </tt> <span class="r">def</span> <span class="fu">connect_adapter</span>(adapter)<tt> </tt> <span class="r">if</span> <span class="iv">@transaction_primitives</span>.key?(adapter)<tt> </tt> raise <span class="s"><span class="dl">&quot;</span><span class="k">Already a primitive for adapter </span><span class="il"><span class="idl">#{</span>adapter<span class="idl">}</span></span><span class="dl">&quot;</span></span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> primitive = <span class="r">if</span> adapter.current_transaction<tt> </tt> adapter.nested_transaction_primitive<tt> </tt> <span class="r">else</span><tt> </tt> adapter.transaction_primitive<tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="iv">@transaction_primitives</span>[adapter] = validate_primitive(primitive)<tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="r">module</span> <span class="cl">NestedTransactions</span><tt> </tt> <span class="r">def</span> <span class="fu">nested_transaction_primitive</span><tt> </tt> <span class="co">DataObjects</span>::<span class="co">NestedTransaction</span>.create_for_uri(normalized_uri, current_connection)<tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="r">class</span> <span class="cl">NestedTransactionConfig</span> &lt; <span class="co">Rails</span>::<span class="co">Railtie</span><tt> </tt> config.after_initialize <span class="r">do</span><tt> </tt> repository.adapter.extend(<span class="co">DataMapper</span>::<span class="co">NestedTransactions</span>)<tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt><span class="r">end</span><tt> </tt><tt> </tt><span class="r">module</span> <span class="cl">DataObjects</span><tt> </tt> <span class="r">class</span> <span class="cl">NestedTransaction</span> &lt; <span class="co">Transaction</span><tt> </tt><tt> </tt> <span class="c"># The host name. Note, this relies on the host name being configured</span><tt> </tt> <span class="c"># and resolvable using DNS</span><tt> </tt> <span class="co">HOST</span> = <span class="s"><span class="dl">&quot;</span><span class="il"><span class="idl">#{</span><span class="co">Socket</span>::gethostbyname(<span class="co">Socket</span>::gethostname)[<span class="i">0</span>]<span class="idl">}</span></span><span class="dl">&quot;</span></span> <span class="r">rescue</span> <span class="s"><span class="dl">&quot;</span><span class="k">localhost</span><span class="dl">&quot;</span></span><tt> </tt> <span class="cv">@@counter</span> = <span class="i">0</span><tt> </tt><tt> </tt> <span class="c"># The connection object for this transaction - must have already had</span><tt> </tt> <span class="c"># a transaction begun on it</span><tt> </tt> attr_reader <span class="sy">:connection</span><tt> </tt> <span class="c"># A unique ID for this transaction</span><tt> </tt> attr_reader <span class="sy">:id</span><tt> </tt><tt> </tt> <span class="r">def</span> <span class="pc">self</span>.create_for_uri(uri, connection)<tt> </tt> uri = uri.is_a?(<span class="co">String</span>) ? <span class="co">URI</span>::parse(uri) : uri<tt> </tt> <span class="co">DataObjects</span>::<span class="co">NestedTransaction</span>.new(uri, connection)<tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="c">#</span><tt> </tt> <span class="c"># Creates a NestedTransaction bound to an existing connection</span><tt> </tt> <span class="c">#</span><tt> </tt> <span class="r">def</span> <span class="fu">initialize</span>(uri, connection)<tt> </tt> <span class="iv">@connection</span> = connection<tt> </tt> <span class="iv">@id</span> = <span class="co">Digest</span>::<span class="co">SHA256</span>.hexdigest(<tt> </tt> <span class="s"><span class="dl">&quot;</span><span class="il"><span class="idl">#{</span><span class="co">HOST</span><span class="idl">}</span></span><span class="k">:</span><span class="il"><span class="idl">#{</span><span class="gv">$$</span><span class="idl">}</span></span><span class="k">:</span><span class="il"><span class="idl">#{</span><span class="co">Time</span>.now.to_f<span class="idl">}</span></span><span class="k">:nested:</span><span class="il"><span class="idl">#{</span><span class="cv">@@counter</span> += <span class="i">1</span><span class="idl">}</span></span><span class="dl">&quot;</span></span>)<tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="r">def</span> <span class="fu">close</span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="r">def</span> <span class="fu">begin</span><tt> </tt> run <span class="s"><span class="dl">%{</span><span class="k">SAVEPOINT &quot;</span><span class="il"><span class="idl">#{</span><span class="iv">@id</span><span class="idl">}</span></span><span class="k">&quot;</span><span class="dl">}</span></span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="r">def</span> <span class="fu">commit</span><tt> </tt> run <span class="s"><span class="dl">%{</span><span class="k">RELEASE SAVEPOINT &quot;</span><span class="il"><span class="idl">#{</span><span class="iv">@id</span><span class="idl">}</span></span><span class="k">&quot;</span><span class="dl">}</span></span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="r">def</span> <span class="fu">rollback</span><tt> </tt> run <span class="s"><span class="dl">%{</span><span class="k">ROLLBACK TO SAVEPOINT &quot;</span><span class="il"><span class="idl">#{</span><span class="iv">@id</span><span class="idl">}</span></span><span class="k">&quot;</span><span class="dl">}</span></span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> private<tt> </tt> <span class="r">def</span> <span class="fu">run</span>(cmd)<tt> </tt> connection.create_command(cmd).execute_non_query<tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt><span class="r">end</span><tt> </tt><tt> </tt></pre></td> </tr></table> <p>I wrote code similar to this with <a href="http://twitter.com/hassox">hassox</a> while at <span class="caps">NZX</span>, big ups to those guys. I&#8217;m working on a proper patch, but haven&#8217;t quite figured out the internals enough. If you know how DataMapper works, please check out and comment on this <a href="http://gist.github.com/598001">sample patch for three dm gems</a>.</p> tag:www.rhnh.net,2008:Post/789 2008-11-14T18:47:00Z 2008-11-14T18:47:46Z Unique data in dm-sweatshop <p><code>dm-sweatshop</code> is how you set up test data for your datamapper apps. Standard practice is to generate random data that follows a pattern:</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="co">User</span>.fix {{<tt> </tt> <span class="sy">:login</span> =&gt; <span class="rx"><span class="dl">/</span><span class="ch">\w</span><span class="k">+</span><span class="dl">/</span></span>.gen<tt> </tt>}}<tt> </tt><tt> </tt>new_user = <span class="co">User</span>.gen<tt> </tt></pre></td> </tr></table> <p>Let&#8217;s not now debate whether or not random data in tests is a good idea. What&#8217;s more important is that the above code should make you uneasy if login is supposed to be unique. There was a hack in sweatshop that would try recreating the data if you had a uniqueness constraint on login and it was invalid, but it was exactly that: a hack. As of a few days ago (what will be 0.9.7), you need to be more explicit if you want unique data. It&#8217;s pretty easy:</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }">include <span class="co">DataMapper</span>::<span class="co">Sweatshop</span>::<span class="co">Unique</span><tt> </tt><tt> </tt><span class="co">User</span>.fix {{<tt> </tt> <span class="sy">:login</span> =&gt; unique { <span class="rx"><span class="dl">/</span><span class="ch">\w</span><span class="k">+</span><span class="dl">/</span></span>.gen }<tt> </tt>}}<tt> </tt></pre></td> </tr></table> <p>Tada! You can also easily get non-random unique data by providing a block with one parameter. <a href="http://github.com/sam/dm-more/tree/master/dm-sweatshop/README.textile">Check the <span class="caps">README</span></a> for this and other cool things you can do.</p>