Trouble with Net::HTTP in Ruby 1.8.7 (released with Ubuntu 10.04)

I stumbled upon an error trying to work with acts_as_solr just after upgrading to Ubuntu 10.04. When trying to start the solr-daemon with

rake solr:start

i suddenly got an error not seen before:

undefined method `closed?' for nil:NilClass
/usr/lib/ruby/1.8/net/http.rb:1060:in `request'
/usr/lib/ruby/1.8/net/http.rb:962:in `request_head'

As the error hints, it’s something to do with the net/http library. After some googling I found out, that it is because Ubuntu 10.04 ships with Ruby 1.8.7, and that this particular version has a bug in the net/http library. Here’s the bug-report and the patch that will help:

http://redmine.ruby-lang.org/issues/show/2708

Hopefully this patch will soon find its way to Canonical’s updates.

Bookmark and Share
Udgivet i Blandet | Tagget , | Skriv en kommentar

My favorite Apache setup for a Rails application

You think 1 site equals 1 Apache Virtual Host? Think again, and read on to figure out why not.

For every Rails application that I deploy into production mode, I usually creates 3 Virtual Host entries in Apache. To illustrate, here is the setup for our dog-site www.gipote.dk:

Note: This setup is using Passenger (aka. mod_rails). If you are not already using it for your Rails production environment, you really should do so. It’s the best thing happened since Rails itself.


    ServerName www.gipote.dk

    DocumentRoot /var/www/rails/gipote_production/public

    RailsEnv production



    ServerName  asset0.gipote.dk
    ServerAlias asset1.gipote.dk
    ServerAlias asset2.gipote.dk
    ServerAlias asset3.gipote.dk

    DocumentRoot /var/www/rails/gipote_production/public

    PassengerEnabled off



    ServerName gi-pote.dk
    ServerAlias *.gi-pote.dk
    ServerAlias *.gipote.dk

    RewriteEngine on
    RewriteRule ^(.*) http://www.gipote.dk$1 [R=301]

    PassengerEnabled off

The *=”" inside my virtual host declations is the WordPress SyntaxHighlighter playing tricks on me. It should just be an *

Some explanation is required…

First of all, I want one and only one entry into our site. This originally came from making it easier to setup Google Analytics (you need to tweak the tracking code to support multiple domains). But it seems that it also improves our SEO rankings – so more power to us.

So the first virtual host only accepts www.gipote.dk. The last virtual host accepts all other variations on gipote.dk (except a few – I’ll come to that) as well as an entirely different domain gi-pote.dk. The last virtual host redirects all these requests to www.gipote.dk.

The last virtual host relly must be placed at the bottom, since it contains wildcards. These wildcards – *.gipote.dk – will catch all hostnames not specified in any virtual host above it. Position is vital.

Now to the second virtual host. It requires a bit more explaining…

Internet Explorer and Firefox has a limit when fetching a webpage concerning concurrent connections to the same host. They will only have 2 open connections to a single host at a time. So if you have a page with many external files – or assets (css, js, images), you may find, that it is slow to load, even if you have a powerful server. The bottleneck is at the client this time.

Rails have a fix for this: Every asset specified in the ERB by the asset helpers image_tag(), stylesheet_link_tag(), etc. Can automatically prepend the URL with an asset host. You need to define the asset host in your config/environments/production.rb file:

config.action_controller.asset_host = "http://asset%d.gipote.dk"

The %d is important. This will make Rails’ asset helpers prepend either of


http://asset0.gipote.dk

http://asset1.gipote.dk

http://asset2.gipote.dk

http://asset3.gipote.dk

to any asset url. It will use exactly these 4 variants, and they will be distributed (almost) evenly out on your assets.

So image_tag(‘logo.png’) will become http://asset2.gipote.dk/images/logo.png (or 0, 1, 3).

The cool thing about this is, that the aforementioned browsers will open 2 connections for each host. So instead of fetching 2 assets concurrently, your browser will now fetch 8 at the same time.

So we need our Apache to deal with this. We cannot rely on the last virtual host to do this, because it will simply redirect to www.gipote.dk. And we don’t want to add these 4 hostnames as alias’ to the top virtual host, because we don’t want our users to be able to access our Rails application by any other hostname than www.gipote.dk.

So I create the second virtual host. Put in all 4 hostnames, point it to my Rails public folder. Oh – and for good performance measure, I disable Passenger (PassengerEnabled off). Otherwise the users would still be able to use the hostnames to access our application.

Bookmark and Share
Udgivet i ruby rails | Tagget , , | Skriv en kommentar

How to make Rails routing case-insensitive

Update 2011-03-10: Brian Marquis pointed out, that I’ve missed showing a good way to actually load the .rb file containing the middleware. This has now been added to the article.

Update 2010-11-06: Rails 3 has changed its routing mechanism. One of the changes involves which environment variable is used as the source for routing. In Rails 2.3.x it was REQUEST_URI, in Rails 3 it is PATH_INFO. I have changed the middleware code to take care of both versions.

At our dog-site www.gipote.dk we have a shop selling dog-tags. The URL for this shop is www.gipote.dk/hundetegn (hundetegn = dog-tag in danish). However one of our marketing-guys have a tendency to capitalize the word “hundetegn”, so that URL reads www.gipote.dk/Hundetegn (with a capital H).

Rails will yield a 404 NOT FOUND on this, simply because it is case sensitive. The Rails core team doesn’t seem to want this changed, but I found a neat way of doing it myself: Using the new Rack Middleware framework.

First the solution:

Create a file called downcase_route_middleware.rb and put it in RAILS_ROOT/lib or wherever you think middleware files ought to go. Fill it with this piece of code:

class DowncaseRouteMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    # Rails 2.3.x routing use REQUEST_URI
    uri_items = env['REQUEST_URI'].split('?')
    uri_items[0].downcase!
    env['REQUEST_URI'] = uri_items.join('?')

    # Rails 3.x routing use PATH_INFO
    env['PATH_INFO'] = env['PATH_INFO'].downcase

    @app.call(env)
  end
end

Now to telling Rails to use this new middleware class. This differs a bit in Rails 2 and 3.

Adding the middleware in Rails 3.x:

Open config/application.rb and find the lines

module YourAppName
  class Application < Rails::Application
    ..bunch of code..
  end
end

Add the middleware code like this:

module YourAppName
  class Application < Rails::Application
    config.autoload_paths << "#{config.root}/app/middleware"
    config.middleware.use "DowncaseRouteMiddleware"

    ..bunch of code..
  end
end

Adding the middleware in Rails 2.3.x:

Open up config/environment.rb, and find the line

Rails::Initializer.run do |config|

  ...a lot of config code...
end

Add the middleware code, so it looks like this afterwards

Rails::Initializer.run do |config|
  config.autoload_paths << "#{config.root}/app/middleware"
  config.middleware.use "DowncaseRouteMiddleware"

  ...a lot of config code...
end

Update: The line:
config.autoload_paths << "#{config.root}/app/middleware"
is my general way of using middleware. I always places all my middleware classes in a folder called app/middleware. The line above enables Rails to automatically search the autoload-path for a file that matches the middleware class used (i.e. "downcase_route_middleware.rb").

If this is the only middleware class that you use, you can simply do a
require "downcase_route_middleware.rb""
somewhere in your config initializers. I just find the above solution more elegant.

Restart your rails application, and it will now accept all kinds of casing on the routing part.

Note: This code only downcases the part of the URI containing namespace, controller, action, and id. It does not touch the querystring parameters, and for a good reason too: The parameter values could contain some context in their casing.

Now for the explanation:

Rails 2.3 introduced Rack middleware. A lot of people have already explained this concept, so instead of going into details, I suggest that you read Pratik's explanation on Rails Guides and Ryan's Railscast on the subject.

Basically, my middleware class gets access to the environment hash and passes it on to the next middleware class on the stack. In between I get to modify the nessecary data. In this case it's the REQUEST_URI (In Rails 3 it's PATH_INFO), since ActionController::Dispatcher uses the URI from here to determine the correct route. By converting the URI to lowercase, ActionController::Dispatcher gets the correct path no matter the case entered by the user.

Bookmark and Share
Udgivet i ruby rails | Tagget , , , , | 10 kommentarer

The woes of libodbc-ruby1.8 and Debian + Ubuntu

Update 05-05-2010: This issue still exists on the new Ubuntu 10.04 LTS (Lucid Lynx). The dist-upgrade will override any version locks that you have made on libodbc-ruby1.8 and install version 0.9997-2 (and lock it). So after a dist-upgrade you will have to remove libodbc-ruby1.8 and install the older version again.

I have the dubious honor of developing on a Rails application, that runs on top of Microsoft SQL Server. This has given some headaches at our most recent system upgrades.

My development machine runs Ubuntu 9.10 (Karmic Koala). Our servers run Debian 5.0 (Lenny).

If you run any of these OS versions and encounter an error like the following:

dbd-odbc-0.2.4/lib/dbd/odbc/driver.rb:36:in `connect':DBI::DatabaseError: INTERN (0) [RubyODBC]Cannot allocate SQLHENV

then help can be found here.

The problem on both OS’es lies in the package libodbc-ruby1.8. But it is actually stranger than you might expect, which I will explain below.

Ubuntu first

We’ll start off with Ubuntu. Fire up Synaptic Package Manger and search for libodbc-ruby1.8. You will see, that the distribution package is versioned 0.9997-2. You need to uninstall this and install version 0.9995-1 instead. You can download version 0.9995-1 here: http://packages.ubuntu.com/hu/hardy/i386/libodbc-ruby1.8/download

Now install it with apt-get and… problem solved. Please note however that your automatic package update will revert to version 0.9997-2 unless you uncheck it before running the update.

Update 19-02-2010: This is also an issue on the 64-bit version of Ubuntu 9.10. Here’s a link where you can download the 0.9995-1 .deb package for both 32-bit AND 64-bit. Thanks Goran.
http://mirrors.kernel.org/ubuntu/pool/universe/libo/libodbc-ruby

Debian next

On Debian it’s the other way around. Debian comes installed with libodbc-ruby1.8 version 0.9995-1 and you need to upgrade this to 0.9997-2. This package is currently only found in unstable, but you can download it here: http://packages.debian.org/sid/i386/libodbc-ruby1.8/download

Install it with apt-get, overriding the previous package.

Hope this saves you from the days of work that I invested in this particular oddity. :-)

Bookmark and Share
Udgivet i ruby rails | Tagget , , | 4 kommentarer

Normalisering af databaser

Dette er en artikel, som jeg har længe haft liggende på Eksperten.dk. Siden Eksperten fik nyt design har det været fuldstændig umuligt at se tabeleksemplerne i artiklen, hvilket gør den stort set ubrugelig. Jeg har derfor flyttet over på min egen blog, hvor jeg bedre kan kontrollere layoutet.

Artiklen henvender sig til dig, der arbejder med databaser. metoderne beskrevet heri er ikke rettet mod bestemte database mærker. faktisk kan de også tages i anvendelse andre steder hvor du bruger datastrukturer, f.eks. kommaseparerede filer, arrays i programmer, osv.

Når du designer databaser med mange tabeller, kan du nemt komme ud for, at flere tabeller “lapper over hinanden” hvad angår de data, som de skal lagre. Det giver følgende problemer:

- Hvis indholdet af et felt i en tabel skal ændres, så skal ændringen også foretages i alle andre tabeller, hvor indholdet forekommer. Hvis dette ikke gøres, så bliver databasen pludselig inkonsistent.
- Hvis en tabel indeholder for mange forskellige data, kan der opstå situationer, hvor man gerne ville gemme records hvor kun en lille delmængde af felterne er udfyldt.

Nok snak for nu. Jeg vil nu vise et eksempel på, hvor galt det kunne gå. Jeg præsenterer et eksempel, som jeg så anvender igennem alle metoderne i artiklen.

Et artikelsystem

Jeg forestiller mig, at jeg har lavet et simpelt artikelsystem. Det kan pt. følgende:

- En bruger kan oprette en artikel, hvor han indtaster en overskrift, en tekst, hans eget navn og e-mail adresse.
- Andre brugere kan nu læse denne artikel og skrive kommentarer til den. En kommentar består af en dato, brugerens navn + e-mail samt kommentarteksten.

Da jeg er nybegynder i datadesign, har jeg lavet det hele i én enkelt tabel:

artikel
+------------+--------------------------+----------------+-----------------+------------------+------------------+-----------------+------------------+---------------------------+
| artikel_id | overskrift               | tekst          | forfatter_navn  | forfatter_email  | kommentar_datoer | kommentar_navne | kommentar_emails | kommentar_tekster         |
+------------+--------------------------+----------------+-----------------+------------------+------------------+-----------------+------------------+---------------------------+
|          1 | Artikel om normalisering | Bla bla bla... | Carsten Gehling | carsten@sarum.dk | 2003-02-10       | Søren Hansen    | sh@email.dk      | Rigtig god artikel...     |
|            |                          |                |                 |                  | 2003-02-11       | Erik Clausen    | erik@clausen.dk  | Det var dog det arg..     |
+------------+--------------------------+----------------+-----------------+------------------+------------------+-----------------+------------------+---------------------------+
|          2 | Python for nybegyndere   | Nu skal i .... | Carsten Gehling | carsten@sarum.dk | 2003-03-05       | Tom Andersen    | tom.and@mail.dk  | Jeg forstår ikke h...     |
|            |                          |                |                 |                  | 2003-03-07       | Erik Clausen    | erik@clausen.dk  | Du har endnu engang..     |
+------------+--------------------------+----------------+-----------------+------------------+------------------+-----------------+------------------+---------------------------+
|          3 | SQL injektion            | Jeg vil kort.. | Squash Guy      | sg@eksperten.dk  | 2003-03-06       | Tom Andersen    | tom.and@mail.dk  | Tankevækkende!!!          |
|            |                          |                |                 |                  | 2003-03-07       | Pia Jørgensen   | pj@somewhere.com | Fin artikel men...        |
|            |                          |                |                 |                  | 2003-03-07       | Erik Clausen    | erik@clausen.dk  | Lige et spørgsmål mere... |
+------------+--------------------------+----------------+-----------------+------------------+------------------+-----------------+------------------+---------------------------+

Til dig der kender lidt mere til datadesign, og som sikkert allerede sidder og korser dig, kan jeg kun sige: Jeg har set konkrete eksempler á la ovenstående.

Metoderne her i artiklen arbejder udfra den tese, at data befinder sig i en bestemt normalform. Hver normalform fortæller noget om hvor velstrukturerede dine data er. Jo højere normalform jo bedre. Jeg vil beskrive de 4 første normalformer fra 0. op til og med 3. normalform og hvordan man kommer fra den ene til den næste. Normalformerne bygger på hinanden således at 3. normalform automatisk kræver at reglerne fra 1. og 2. normalform også er overholdt.

0. normalform (0NF)
Ovenstående eksempel befinder sig i 0. normalform (0NF). Dette er den løseste form, der ikke stiller nogle krav til opbygningen af dine data. Til gengæld kan du heller ikke gøre så meget ved data i denne form, uden at det kræver et stort arbejde.

Der er ingen regler for denne normalform, og derfor er der heller ikke meget mere at beskrive om den. Til gengæld vil det næste afsnit fortælle, hvilke problemer, der løses ved at ændre sine data, så de overholder reglerne for 1. normalform.

1. normalform (1NF)

Som tabellen “artikel” er opbygget lige nu, er det meget svært at udskille en enkelt kommentar fra en given artikel. Faktisk kan det ikke lade sig gøre med en SQL-sætning alene. Der skal programmeringsarbejde til. Først skal hele artikel-recorden indlæses. Derefter skal felterne “kommentar_datoer”, “kommentar_navne”, “kommentar_emails” og “kommentar_tekster” splittes op vha. programmering og gennemløbes.

Den måde som kommentarerne gemmes på i mit eksempel kaldes “repeating groups”. Det betyder, at du har en gruppe data, som gentages i den samme record. Problemet med repeating groups er, at du som tidligere nævnt ikke kan identificere én enkelt af disse datagrupper entydigt.

Reglen for første 1. normalform (1NF) går i al sin enkelthed ud på, at dine data ikke må have “repeating groups”. Dvs. alle data skal kunne identificeres entydigt udfra en nøgle (f.eks. primærnøglen).

Det var så reglen. Hvordan får vi så ændret vores data til at opfylde denne regel?

Metoden går ud på, at vi udskiller alle felterne i vores repeating groups i en separat tabel. Samtidig medtager vi primærnøglen fra tabellen “artikel” som en fremmednøgle. Dermed kan vores records i den nye tabel referere til de records i “artikel”, som de hører til. Det ser således ud:

artikel
+------------+--------------------------+----------------+-----------------+------------------+
| artikel_id | overskrift               | tekst          | forfatter_navn  | forfatter_email  |
+------------+--------------------------+----------------+-----------------+------------------+
|          1 | Artikel om normalisering | Bla bla bla... | Carsten Gehling | carsten@sarum.dk |
+------------+--------------------------+----------------+-----------------+------------------+
|          2 | Python for nybegyndere   | Nu skal i .... | Carsten Gehling | carsten@sarum.dk |
+------------+--------------------------+----------------+-----------------+------------------+
|          3 | SQL injektion            | Jeg vil kort.. | Squash Guy      | sg@eksperten.dk  |
+------------+--------------------------+----------------+-----------------+------------------+

kommentar
+------------+------------------+-----------------+------------------+---------------------------+
| artikel_id | dato             | navn            | email            | tekst                     |
+------------+------------------+-----------------+------------------+---------------------------+
|          1 | 2003-02-10       | Søren Hansen    | sh@email.dk      | Rigtig god artikel...     |
+------------+------------------+-----------------+------------------+---------------------------+
|          1 | 2003-02-11       | Erik Clausen    | erik@clausen.dk  | Det var dog det arg..     |
+------------+------------------+-----------------+------------------+---------------------------+
|          2 | 2003-03-05       | Tom Andersen    | tom.and@mail.dk  | Jeg forstår ikke h...     |
+------------+------------------+-----------------+------------------+---------------------------+
|          2 | 2003-03-07       | Erik Clausen    | erik@clausen.dk  | Du har endnu engang..     |
+------------+------------------+-----------------+------------------+---------------------------+
|          3 | 2003-03-06       | Tom Andersen    | tom.and@mail.dk  | Tankevækkende!!!          |
+------------+------------------+-----------------+------------------+---------------------------+
|          3 | 2003-03-07       | Pia Jørgensen   | pj@somewhere.com | Fin artikel men...        |
+------------+------------------+-----------------+------------------+---------------------------+
|          3 | 2003-03-07       | Erik Clausen    | erik@clausen.dk  | Lige et spørgsmål mere... |
+------------+------------------+-----------------+------------------+---------------------------+

Begge tabeller opfylder nu kravene for 1NF.

Når du designer tabeller i et relationel databasesystem som MySQL, SQL Server, Access el. så vil du normalt havne i 1NF med det samme. Værktøjerne i systemerne lægger automatisk op til det. Men det er alligevel vigtigt at beskrive, hvad minimumskravene er for 1NF, ellers kan du nemt falde i.

Bemærk, at tabellen “kommentar” ikke har en simpel primærnøgle. Dette er bevidst gjort, for at belyse et andet aspekt ved datadesign: En primærnøgle må gerne være sammensat af flere felter. I dette tilfælde består primærnøglen faktisk af felterne “artikel_id”, “dato”, “email” og “tekst”. Det virker måske grotesk, og det får vi også gjort noget i næste afsnit.

2. normalform (2NF)

2NF går ind og ser på sammensatte nøgler, dvs. nøgler der består af to eller flere felter i en tabel.

Det er som sagt helt i orden at have sammensatte nøgler. De fleste erfarne database designere vælger automatisk at undgå det, men de kan have deres nytte – et emne der ligger udenfor denne artikel.

Reglen for 2NF siger, at alle felter i en tabel skal være afhængig af hele nøglen. Det betyder, at et felt ikke må kunne findes med blot af felterne fra den sammensatte nøgle.

Vi ser nu på tabellerne fra før:

artikel
I denne tabel består primærnøglen alene af feltet “artikel_id”. Da der ikke er tale om en sammensat nøgle, så opfylder tabellen automatisk kravene for 2NF.

kommentar
Som tidligere beskrevet, så består primærnøglen for denne tabel af felterne “artikel_id”, “dato”, “email” og “tekst”. Så langt så godt. Men der er et problem. Faktisk kan feltet “navn” findes alene udfra feltet “email”. Dvs. “navn” er ikke fuldt afhængigt af primærnøglen – du behøver ikke at kende værdien af alle felterne i primærnøglen, for at finde ud af, at forfatteren hedder “Tom Andersen”. Dukal bare vide, at hans e-mail adresse er “tom.and@mail.dk”.

Måden at løse dette problem ligner lidt måden fra før:
1) Lav en ny tabel (vi kalder den “bruger”)
2) Indsæt de felter fra den gamle tabel, som kun er delvist afhængig af primærnøglen (dvs. “navn”)
3) Indsæt de felter fra primærnøglen i den gamle tabel, som felterne fra 2) er afhængige af – disse bliver den nye tabels primærnøgle (i vores tilfælde er dette “email”)

Det giver følgende resultat:

kommentar
+------------+------------------+------------------+---------------------------+
| artikel_id | dato             | email            | tekst                     |
+------------+------------------+------------------+---------------------------+
|          1 | 2003-02-10       | sh@email.dk      | Rigtig god artikel...     |
+------------+------------------+------------------+---------------------------+
|          1 | 2003-02-11       | erik@clausen.dk  | Det var dog det arg..     |
+------------+------------------+------------------+---------------------------+
|          2 | 2003-03-05       | tom.and@mail.dk  | Jeg forstår ikke h...     |
+------------+------------------+------------------+---------------------------+
|          2 | 2003-03-07       | erik@clausen.dk  | Du har endnu engang..     |
+------------+------------------+------------------+---------------------------+
|          3 | 2003-03-06       | tom.and@mail.dk  | Tankevækkende!!!          |
+------------+------------------+------------------+---------------------------+
|          3 | 2003-03-07       | pj@somewhere.com | Fin artikel men...        |
+------------+------------------+------------------+---------------------------+
|          3 | 2003-03-07       | erik@clausen.dk  | Lige et spørgsmål mere... |
+------------+------------------+------------------+---------------------------+

bruger
+------------------+-----------------+
| email            | navn            |
+------------------+-----------------+
| sh@email.dk      | Søren Hansen    |
+------------------+-----------------+
| erik@clausen.dk  | Erik Clausen    |
+------------------+-----------------+
| tom.and@mail.dk  | Tom Andersen    |
+------------------+-----------------+
| pj@somewhere.com | Pia Jørgensen   |
+------------------+-----------------+

I tabellen “kommentar” er feltet “email” nu fremmednøgle til tabellen “bruger”. I tabellen “bruger” er feltet “email” gjort til primærnøglen.

Skulle “Erik Clausen” nu pludselig beslutte sig for at ændre sit efternavn, er det blevet noget lette at administrere. Det skal kun gøre ét sted frem for tre steder tidligere.

3. normalform (3NF)

3NF kræver, at et felt kun er afhængig af primærnøglen. Selvom alle felterne i en tabel overholder 2NF eller at primærnøglen slet ikke er sammensat, så kan der være tilfælde, hvor indholdet af et felt kan bestemmes alene udfra et andet felt i tabellen, som ikke selv er en del af primærnøglen.

Vi ser problemet i tabellen “artikel”. “forfatter_email” er udelukkende afhængig af primærnøglen “artikel_id”, men “forfatter_navn” kan faktisk findes både udfra “artikel_id” og “forfatter_email”. Så tabellen overholder ikke reglen for 3NF. I lærebøgerne kaldes det at “forfatter_navn er transitiv afhængig af artikel_id”. Dvs. forfatter_navn er afhængig af forfatter_email, som så igen er afhængig af artikel_id.

Dette løser vi således:

1) Lav en ny tabel
2) Indsæt de felter fra den gamle tabel, som kun er transitiv afhængig af primærnøglen (dvs. “forfatter_navn”)
3) Indsæt de felter fra primærnøglen i den gamle tabel, som felterne fra 2) er afhængige af – disse bliver den nye tabels primærnøgle (i vores tilfælde er dette “email”)

Nu er det jo så fikst, at vi allerede har en tabel med ovenstående egenskaber – nemlig tabellen “bruger”. Så den tillader vi os at genbruge:

artikel
+------------+--------------------------+----------------+------------------+
| artikel_id | overskrift               | tekst          | email            |
+------------+--------------------------+----------------+------------------+
|          1 | Artikel om normalisering | Bla bla bla... | carsten@sarum.dk |
+------------+--------------------------+----------------+------------------+
|          2 | Python for nybegyndere   | Nu skal i .... | carsten@sarum.dk |
+------------+--------------------------+----------------+------------------+
|          3 | SQL injektion            | Jeg vil kort.. | sg@eksperten.dk  |
+------------+--------------------------+----------------+------------------+

bruger
+------------------+-----------------+
| email            | navn            |
+------------------+-----------------+
| carsten@sarum.dk | Carsten Gehling |
+------------------+-----------------+
| sg@eksperten.dk  | Squash Guy      |
+------------------+-----------------+
| + alle de øvrige brugere fra før.. |
+------------------+-----------------+

Nu overholder alle tabellerne både 1., 2. og 3. normalform. Dermed er du også kommet ud over de problemer, som jeg beskrev i starten:

- Hvis en bruger tidligere skulle skifte navn, så skulle det både ændres i alle records, hvor brugeren havde oprettet artikler og skrevet kommentarer. Nu skal navnet bare ændres én gang i tabellen bruger.
- Det var ikke muligt at oprette en bruger uden samtidig at oprette en artikel. Hvis du gjorde, så ville recorden i den gamle artikel tabel indeholde mangle tomme felter. Det er nu muligt brugeren skal bare oprettes i tabellen bruger.

Afslutning

Der kunne godt laves andre optimeringer på tabellerne. F.eks. er det en god idé at oprette et numerisk felt f.eks. kaldet “bruger_id” som primærnøgle i “bruger” og fremmednøgle i “artikel”. Ellers er det sværere at opdatere en brugers e-mail. Men det er ikke et krav for 1NF, 2NF eller 3NF.

Metoden bringer adskiller data i yderste konsekvens. Det betyder også, at visse dataudtræk (SELECT) kræver, at to eller flere tabeller join’es, en operation der tager længere tid end en simpel select. Derfor kan du somme tider komme ud for, at det godt kan betale sig at bryde med en af reglerne af hensyn til performance.

MEN: Lær først reglerne grundigt. Så er du også mere kompetent til at bryde dem de rigtige steder. :-)

God fornøjelse.

Bookmark and Share
Udgivet i Blandet | Skriv en kommentar

Reclaim control of your application with Cucumber

As I’ve written earlier(danish), I’ve arrived rather late to the test-driven development-train. As a result I have a large application, developed in Rails, but without utilizing the test framework. Granted I have lately added some unit-tests, but in the whole, I would deem the application uncovered by tests.

Such a beast is difficult to handle. To upgrade it to support the latest Rails version (2.3.5 in this time of writing) is like jumping of a plane and hope to catch a floating parachute on your way down. On the other hand, writing unit-tests and functional tests to cover all current functionality is a huge task, that is discouraging simply by its presence. To put it short: The application has grown beyond my control.

So how to tackle this situation? I want the entire application (or most of at least) covered by test, but I will not be able to spare the next 3 months doing nothing but writing unit and functional tests.

Enter Cucumber.

With Cucumber I am able to create happy paths for all the use cases in my application. I know how each area of the application is supposed to be used, and by outlining these uses in Cucumber features and scenarios, I get a basic coverage of all functionality.

This is a very top-down approach. I don’t get to test each small functionality separately. And even the Cucumber guys encourage coders to write more than just happy paths (adding also scenarios that are supposed to test for error messages). But at least, if something breaks, I will know about it. So when I change something in my application, I run all my cucumber features. If one of the scenarios fail, I can now focus on writing more detailed tests for this particular part of the application.

A big thanks to Aslak Hellesøy and friends for helping me reclaim control of my application!

Bookmark and Share
Udgivet i Blandet, ruby rails | Tagget , , , | Skriv en kommentar

Simple Amazon S3 file synchronize for Rails application (without the need of Capistrano)

So you have a nice Rails-app running and you want all your static assets to reside on Amazon S3?

There are other good solutions on the web, but most of what I’ve seen requires that you also use Capistrano for deploying your app. If you have not yet mastered that beast (just like me), you will like this little rake task, that I’ve made.

Just copy/paste this code into your favorite editor, change the 4 parameters in the top, and save it in lib/tasks/aws.rake.

Then you can do this:

# rake aws:sync

This will upload all folders and files from your public folder into your Amazon S3 bucket.

The script creates a file called .aws_cache in the root of your application folder. This is used so that additional calls to rake aws:sync only uploads new and modified files.

If you have already uploaded your files before applying this script, you can start by calling

# rake aws:build_cache

This will build the local cachefiles without uploading anything to Amazon S3.

Here’s the code:

require 'find'
require 'digest/md5'
require 'yaml'

#########################################################
# Configuration - these lines are all you need to edit
ACCESS_KEY_ID     = "your-own-aws-access-key"
SECRET_ACCESS_KEY = "and-the-secret-key"
BUCKET             = "name-of-your-bucket"

# If you have any subfolders inside "public", that you do not want to place on AWS, list them here
IGNORE_FOLDERS    = %w(upload UserFiles videos)
#########################################################

class AwsCache
  def initialize
    @filename = ".aws_cache"
    load
  end

  def clear
    @cache = {}
  end

  def load
    clear
    if File.exists?(@filename)
      @cache = YAML::load_file(@filename)
    end
  end

  def save
    File.open(@filename, "w") do |f|
      f.puts @cache.to_yaml
    end
  end

  def add(path)
    puts "add to cache: #{path}"
    @cache[path] = checksum(path)
  end

  def equal?(path)
    c = checksum(path)
    @cache[path] == c
  end

  protected
  def checksum(path)
    Digest::MD5.file(path).hexdigest
  end
end

namespace :aws do
  desc "Synchronize public folder"
  task :sync => :environment do
    init

    AWS::S3::Base.establish_connection!(
      :access_key_id     => ACCESS_KEY_ID,
      :secret_access_key => SECRET_ACCESS_KEY
    )

    bucket = AWS::S3::Bucket.find(@bucket)

    loop_folder()

    @aws_cache.save
  end

  desc "Rebuild local cache file"
  task :build_cache => :environment do
    init

    absolute_folder = File.join(@base_folder, "")

    Find.find(absolute_folder) do |path|
      if FileTest.directory?(path)
        if File.basename(path)[0] == ?.
          Find.prune
        elsif IGNORE_FOLDERS.include?(File.basename(path))
          Find.prune
        end
      else
        @aws_cache.add(path)
      end
    end

    @aws_cache.save
  end
end

def init
  @base_folder = File.join(RAILS_ROOT, "public")
  @bucket      = BUCKET
  @aws_cache   = AwsCache.new
end

def loop_folder(folder = "")
  absolute_folder = File.join(@base_folder, folder)

  total_size = 0

  Find.find(absolute_folder) do |path|
    if FileTest.directory?(path)
      if File.basename(path)[0] == ?.
        Find.prune
      elsif IGNORE_FOLDERS.include?(File.basename(path))
        Find.prune
      else
        next
      end
    else
      s3_store(path)
    end
  end
end

def s3_store(path)
  s3_path = path.gsub("#{@base_folder}/", '')

  if transfer?(path)
    AWS::S3::S3Object.store(
      s3_path,
      open(path),
      @bucket,
      :access => :public_read)

    puts "Stored into AWS: #{AWS::S3::S3Object.url_for(s3_path, @bucket)[/[^?]+/]}"
  end
end

def transfer?(path)
  return false if @aws_cache.equal?(path)

  @aws_cache.add(path)
  return true
end

Feel free to contact me, if you have any questions, problems, suggestions for improvement.

Bookmark and Share
Udgivet i ruby rails | Tagget , , , , | Skriv en kommentar

Looking forward to Ubuntu 9.10

http://www.ubuntu.com/

Bookmark and Share
Udgivet i Blandet | Tagget | Skriv en kommentar

Cloud As A Service

Vil du virkelig være noget i IT-verdenen lige nu, så skal du helst tilbyde alle dine service-ydelser som – nåja – “as a service”. Seneste skud på stammen er “Test as-a-service”.

Og så “Cloud”. Det er jo gået hen og blevet det nye XMl – løsningen på alle IT-problemer. Det er virkelig et godt eksempel på gammel vin på nye flasker.

Lyder jeg som en gammel bitter mand? Måske. Jeg vil snarere sige, at jeg udviser rettidig omhu, og ikke springer på hvert nyt lyntog.

Bookmark and Share
Udgivet i Blandet | Tagget | Skriv en kommentar

Test test og atter test

Jeg er nu endelig kommet på den rette vej med TDD. Jeg må jo indrømme, at TDD for mig indtil nu har været “kode først, tests bagefter”, men jeg så forleden lige pludselig “lyset”. Det har også kun taget 2¾ år med Rails at nå dertil… ;-)

Hvad gjorde så udslaget for mig? Jo det var faktisk, da jeg læste denne blog http://code.isdangero.us/posts/My-test-driven-Ruby-setup

Jeg kunne godt lide syntaksen i Shoulda. Men det var især Machinist og den lette måde at lave test-data på, som gav mig en “aha-oplevelse”. Fixtures er besværlige at lave, og det er nemt at knække eksisterende tests, når man tilføjer flere tests. Machinist ændrer på alt det, og det gik pludselig op for mig, at det netop var dét, der var bremsen for min omvenden.

Men for pokker hvor er der pludselig mange forskellige frameworks/ libraries til TDD/BDD. Det er jo nærmest en jungle. Det er sjovt, at hvor de fleste bare accepterer ActiveRecord og de øvrige konventioner i Rails, så bruger rigtig mange tilsyneladende alt muligt andet end Rails’ egent test-suite.

Hvilken kombi bruger i og hvorfor? Hvor nemt har i ved at teste først og kode bagefter? Jeg er gammel i gårde, og jeg må indrømme, at det er en af de steder, hvor jeg har svært ved at ændre gamle vaner.

- Carsten

Bookmark and Share
Udgivet i Blandet | Tagget , , , , | Skriv en kommentar