Ejemplo de Uso de DataMapper: Canciones de Sinatra

Donde

Enlaces

  1. Documentación del módulo DataMapper en RubyDoc
  2. https://github.com/crguezl/datamapper_example
  3. https://github.com/crguezl/datamapper-intro

La Clase Song

[~/sinatra/sinatra-datamapper-jump-start(master)]$ cat song.rb 
require 'dm-core'
require 'dm-migrations'

class Song
  include DataMapper::Resource
  property :id, Serial
  property :title, String
  property :lyrics, Text
  property :length, Integer
  
end
The Song model is going to need to be persistent, so we'll include DataMapper::Resource.

The convention with model names is to use the singular, not plural version... but that's just the convention, we can do whatever we want.

configure do
  enable :sessions
  set :username, 'frank'
  set :password, 'sinatra'
end

DataMapper.finalize

DataMapper.finalize
This method performs the necessary steps to finalize DataMapper for the current repository. It should be called after loading all models and plugins. It ensures foreign key properties and anonymous join models are created. These are otherwise lazily declared, which can lead to unexpected errors. It also performs basic validity checking of the DataMapper models.

Mas código de Song.rb

get '/songs' do
  @songs = Song.all
  slim :songs
end

get '/songs/new' do
  halt(401,'Not Authorized') unless session[:admin]
  @song = Song.new
  slim :new_song
end

get '/songs/:id' do
  @song = Song.get(params[:id])
  slim :show_song
end

get '/songs/:id/edit' do
  @song = Song.get(params[:id])
  slim :edit_song
end

Song.create

If you want to create a new resource with some given attributes and then save it all in one go, you can use the #create method:

post '/songs' do  
  song = Song.create(params[:song])
  redirect to("/songs/#{song.id}")
end

put '/songs/:id' do
  song = Song.get(params[:id])
  song.update(params[:song])
  redirect to("/songs/#{song.id}")
end

delete '/songs/:id' do
  Song.get(params[:id]).destroy
  redirect to('/songs')
end

Una sesión con pry probando DataMapper

[~/sinatra/sinatra-datamapper-jump-start(master)]$ pry
[1] pry(main)> require 'sinatra'
=> true
[2] pry(main)> require './song'
=> true

DataMapper.setup

We must specify our database connection.

We need to make sure to do this before you use our models, i.e. before we actually start accessing the database.

  # If you want the logs displayed you have to do this before the call to setup
  DataMapper::Logger.new($stdout, :debug)

  # An in-memory Sqlite3 connection:
  DataMapper.setup(:default, 'sqlite::memory:')

  # A Sqlite3 connection to a persistent database
  DataMapper.setup(:default, 'sqlite:///path/to/project.db')

  # A MySQL connection:
  DataMapper.setup(:default, 'mysql://user:password@hostname/database')

  # A Postgres connection:
  DataMapper.setup(:default, 'postgres://user:password@hostname/database')
Note: that currently you must setup a :default repository to work with DataMapper (and to be able to use additional differently named repositories). This might change in the future.

In our case:

[4] pry(main)> pry(main)> DataMapper.setup(:default,'sqlite:development.db')

Multiple Data-Store Connections

DataMapper sports a concept called a context which encapsulates the data-store context in which you want operations to occur. For example, when you setup a connection you are defining a context known as :default

   DataMapper.setup(:default, 'mysql://localhost/dm_core_test')
If you supply another context name, you will now have 2 database contexts with their own unique loggers, connection pool, identity map....one default context and one named context.

 DataMapper.setup(:external, 'mysql://someother_host/dm_core_test')
To use one context rather than another, simply wrap your code block inside a repository call. It will return whatever your block of code returns.

 DataMapper.repository(:external) { Person.first }
 # hits up your :external database and retrieves the first Person
This will use your connection to the :external data-store and the first Person it finds. Later, when you call .save on that person, it'll get saved back to the :external data-store; An object is aware of what context it came from and should be saved back to.

El Objeto DataMapper::Adapters

=> #<DataMapper::Adapters::SqliteAdapter:0x007fad2c0f6a50
 @field_naming_convention=DataMapper::NamingConventions::Field::Underscored,
 @name=:default,
 @normalized_uri=
  #<DataObjects::URI:0x007fad2c0f62a8
   @fragment="{Dir.pwd}/development.db",
   @host="",
   @password=nil,
   @path=nil,
   @port=nil,
   @query=
    {"scheme"=>"sqlite3",
     "user"=>nil,
     "password"=>nil,
     "host"=>"",
     "port"=>nil,
     "query"=>nil,
     "fragment"=>"{Dir.pwd}/development.db",
     "adapter"=>"sqlite3",
     "path"=>nil},
   @relative=nil,
   @scheme="sqlite3",
   @subscheme=nil,
   @user=nil>,
 @options=
  {"scheme"=>"sqlite3",
   "user"=>nil,
   "password"=>nil,
   "host"=>"",
   "port"=>nil,
   "query"=>nil,
   "fragment"=>"{Dir.pwd}/development.db",
   "adapter"=>"sqlite3",
   "path"=>nil},
 @resource_naming_convention=
  DataMapper::NamingConventions::Resource::UnderscoredAndPluralized>

Creando las tablas con DataMapper.auto_migrate!

We can create the table by issuing the following command:
[4] pry(main)> DataMapper.auto_migrate!
  1. This will issue the necessary CREATE statements (DROPing the table first, if it exists) to define each storage according to their properties.
  2. After auto_migrate! has been run, the database should be in a pristine state.
  3. All the tables will be empty and match the model definitions.

DataMapper.auto_upgrade!

This wipes out existing data, so you could also do:
DataMapper.auto_upgrade!

  1. This tries to make the schema match the model.
  2. It will CREATE new tables, and add columns to existing tables.
  3. It won't change any existing columns though (say, to add a NOT NULL constraint) and it doesn't drop any columns.
  4. Both these commands also can be used on an individual model (e.g. Song.auto_migrate!)

Métodos de la Clase Mapeada

[5] pry(main)> song = Song.new
=> #<Song @id=nil @title=nil @lyrics=nil @length=nil @released_on=nil>
[6] pry(main)> song.save
=> true
[7] pry(main)> song
=> #<Song @id=1 @title=<not loaded> @lyrics=<not loaded> @length=<not loaded> @released_on=<not loaded>>
[8] pry(main)> song.title = "My Way"
=> "My Way"
[9] pry(main)> song.lyrics
=> nil
[10] pry(main)> song.lyrics = "And now, the end is near ..."
=> "And now, the end is near ..."
[11] pry(main)> song.length = 435
=> 435
[42] pry(main)> song.save
=> true
[43] pry(main)> song
=> #<Song @id=1 @title="My Way" @lyrics="And now, the end is near ..." @length=435 @released_on=nil>

El método create

If you want to create a new resource with some given attributes and then save it all in one go, you can use the #create method.
[28] pry(main)> Song.create(title: "Come fly with me", lyrics: "Come fly with me, let's fly, let's fly away ...", length: 199) 
=> #<Song @id=2 @title="Come fly with me" @lyrics="Come fly with me, let's fly, let's fly away ..." @length=199 @released_on=<not loaded>>
  1. If the creation was successful, #create will return the newly created DataMapper::Resource
  2. If it failed, it will return a new resource that is initialized with the given attributes and possible default values declared for that resource, but that's not yet saved
  3. To find out wether the creation was successful or not, you can call #saved? on the returned resource
  4. It will return true if the resource was successfully persisted, or false otherwise

first_or_create

If you want to either find the first resource matching some given criteria or just create that resource if it can't be found, you can use #first_or_create.

s = Song.first_or_create(:title => 'New York, New York')
This will first try to find a Song instance with the given title, and if it fails to do so, it will return a newly created Song with that title.

If the criteria you want to use to query for the resource differ from the attributes you need for creating a new resource, you can pass the attributes for creating a new resource as the second parameter to #first_or_create, also in the form of a #Hash.

s = Song.first_or_create({ :title => 'My Way' }, { :lyrics => '... the end is not near' })

This will search for a Song named 'My Way' and if it can't find one, it will return a new Song instance with its name set to 'My Way' and the lyrics set to .. the end is not near

  1. You can see that for creating a new resource, both hash arguments will be merged so you don't need to specify the query criteria again in the second argument Hash that lists the attributes for creating a new resource
  2. However, if you really need to create the new resource with different values from those used to query for it, the second Hash argument will overwrite the first one.

s = Song.first_or_create({ :title => 'My Way' }, {
  :title  => 'My Way Home',
  :lyrics => '... the end is not near'
})
This will search for a Song named 'My Way' but if it fails to find one, it will return a Song instance with its title set to 'My Way Home' and its lyrics set to '... the end is not near'.

Comprobando con sqlite3

Podemos abrir la base de datos con el gestor de base de datos y comprobar que las tablas y los datos están allí:

[~/sinatra/sinatra-datamapper-jump-start(master)]$ sqlite3 development.db 
SQLite version 3.7.11 2012-03-20 11:35:50
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .schema
CREATE TABLE "songs" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 
		      "title" VARCHAR(50), "lyrics" TEXT, "length"
		      INTEGER, "released_on" TIMESTAMP);
sqlite> select * from songs;
1|My Way|And now, the end is near ...|435|
2|Come fly with me|Come fly with me, let's fly, let's fly away ...|199|
sqlite>

Búsquedas y Consultas

DataMapper has methods which allow you to grab a single record by key, the first match to a set of conditions, or a collection of records matching conditions.

song  = Song.get(1)                     # get the song with primary key of 1.
song  = Song.get!(1)                    # Or get! if you want an ObjectNotFoundError on failure
song  = Song.first(:title => 'Girl')    # first matching record with the title 'Girl'
song  = Song.last(:title => 'Girl')     # last matching record with the title 'Girl'
songs = Song.all                        # all songs

[29] pry(main)> Song.count
=> 2
[30] pry(main)> Song.all
=> [#<Song @id=1 @title=nil @lyrics=<not loaded> @length=nil @released_on=nil>, #<Song @id=2 @title="Come fly with me" @lyrics=<not loaded> @length=199 @released_on=nil>]
[31] pry(main)> Song.get(1)
=> #<Song @id=1 @title=nil @lyrics=<not loaded> @length=nil @released_on=nil>
[32] pry(main)> Song.first
=> #<Song @id=1 @title=nil @lyrics=<not loaded> @length=nil @released_on=nil>
[33] pry(main)> Song.last
=> #<Song @id=2 @title="Come fly with me" @lyrics=<not loaded> @length=199 @released_on=nil>
[35] pry(main)> x = Song.first(title: 'Come fly with me')
=> #<Song @id=2 @title="Come fly with me" @lyrics=<not loaded> @length=199 @released_on=nil>

[44] pry(main)> y = Song.first(title: 'My Way')
=> #<Song @id=1 @title="My Way" @lyrics=<not loaded> @length=435 @released_on=nil>
[45] pry(main)> y.length
=> 435
[46] pry(main)> y.update(length: 275)
=> true

En Sqlite3:

sqlite> select * from songs;
1|My Way|And now, the end is near ...|275|
2|Come fly with me|Come fly with me, let's fly, let's fly away ...|199|

Borrando

[47] pry(main)> Song.create(title: "One less lonely girl")
=> #<Song @id=3 @title="One less lonely girl" @lyrics=<not loaded> @length=<not loaded> @released_on=<not loaded>>
[48] pry(main)> Song.last.destroy
=> true
[49] pry(main)> Song.all
=> [#<Song @id=1 @title="My Way" @lyrics=<not loaded> @length=275 @released_on=nil>, #<Song @id=2 @title="Come fly with me" @lyrics=<not loaded> @length=199 @released_on=nil>]

Búsqueda con Condiciones

Rather than defining conditions using SQL fragments, we can actually specify conditions using a hash.

The examples above are pretty simple, but you might be wondering how we can specify conditions beyond equality without resorting to SQL. Well, thanks to some clever additions to the Symbol class, it's easy!

exhibitions = Exhibition.all(:run_time.gt => 2, :run_time.lt => 5)
# => SQL conditions: 'run_time > 1 AND run_time < 5'
Valid symbol operators for the conditions are:

gt    # greater than
lt    # less than
gte   # greater than or equal
lte   # less than or equal
not   # not equal
eql   # equal
like  # like
Veamos un ejemplo de uso con nuestra clase Song:
[31] pry(main)> Song.all.each do |s|
[31] pry(main)*   s.update(length: rand(400))
[31] pry(main)* end  
=> [#<Song @id=1 @title="My Way" @lyrics=<not loaded> @length=122 @released_on=nil>,
   #<Song @id=2 @title="Come fly with me" @lyrics=<not loaded> @length=105 @released_on=nil>,
   #<Song @id=4 @title="Girl from Ipanema" @lyrics=<not loaded> @length=389 @released_on=nil>]
[32] pry(main)> long = Song.all(:length.gt => 120)
=> [#<Song @id=1 @title="My Way" @lyrics=<not loaded> @length=122 @released_on=nil>,
   #<Song @id=4 @title="Girl from Ipanema" @lyrics=<not loaded> @length=389 @released_on=nil>]

Insertando SQL

Sometimes you may find that you need to tweak a query manually:
[40] pry(main)> songs = repository(:default).adapter.select('SELECT title FROM songs WHERE length >= 110')
=> ["My Way", "Girl from Ipanema"]
Note that this will not return Song objects, rather the raw data straight from the database

main.rb

[~/sinatra/sinatra-datamapper-jump-start(master)]$ cat main.rb
require 'sinatra'
require 'slim'
require 'sass'
require './song'

configure do
  enable :sessions
  set :username, 'frank'
  set :password, 'sinatra'
end

configure :development do
  DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/development.db")
end

configure :production do
  DataMapper.setup(:default, ENV['DATABASE_URL'])
end

get('/styles.css'){ scss :styles }

get '/' do
  slim :home
end

get '/about' do
  @title = "All About This Website"
  slim :about
end

get '/contact' do
  slim :contact
end

not_found do
  slim :not_found
end

get '/login' do
  slim :login
end

post '/login' do
  if params[:username] == settings.username && params[:password] == settings.password
    session[:admin] = true
    redirect to('/songs')
  else
    slim :login
  end
end

get '/logout' do
  session.clear
  redirect to('/login')
end



Subsecciones
Casiano Rodriguez León 2015-01-07