Create, Save, Update and Destroy
This page describes the basic methods to use when creating, saving, updating and destroying resources with DataMapper. Some of DataMapper’s concepts might be confusing to users coming from ActiveRecord for example. For this reason, we start with a little background on the usage of bang vs. no-bang methods in DataMapper, followed by ways of manipulating the rules DataMapper abides when it comes to raising exceptions in case some persistence operation went wrong.
Bang(!) or no bang methods
This page is about creating, saving, updating and destroying resources with
DataMapper. The main methods to achieve these tasks are #create
, #save
,
#update
and #destroy
. All of these methods have bang method equivalents
which operate in a slightly different manner. DataMapper follows the general
ruby idiom when it comes to using bang or non-bang methods. A detailed explanation
of this idiom can be found at
When you call a non-bang method like #save
, DataMapper will invoke all callbacks
defined for resources of the model. This means that it will have to load all affected
resources into memory in order to be able to execute the callbacks on each one of them.
This can be considered the safe version, without the bang(!). While it sometimes may
not be the best way to achieve a particular goal (bad performance), it’s as safe as it
gets. In fact, if dm-validations
are required and active, calling the non-bang version
of any of these methods, will make sure that all validations are being run too.
Sometimes though, you either don’t need the extra safety you get from dm-validations
,
or you don’t want any callbacks to be invoked at all. In situations like this, you can use
the bang(!) versions of the respective methods. You will probably find yourself using these
unsafe methods when performing internal manipulation of resources as opposed to, say,
persisting attribute values entered by users (in which case you’d most likely use the safe
versions). If you call #save!
instead of #save
, no callbacks and no validations will
be run. DataMapper just assumes that you know what you do. This can also have severe impact
on the performance of some operations. If you’re calling #save!
, #update!
or #destroy!
on a (large) DataMapper::Collection
, this will result in much better performance than calling
the safe non-bang counterparts. This is because DataMapper won’t load the collection into
memory because it won’t execute any resource level callbacks or validations.
While the above examples mostly used #save
and #save!
to explain the different behavior,
the same rules apply for #create!
, #save!
, #update!
and #destroy!
. The safe non-bang
methods will always execute all callbacks and validations, and the unsafe bang(!) methods
never will.
Raising an exception when save fails
By default, datamapper returns true
or false
for all operations manipulating
the persisted state of a resource (#create
, #save
, #update
and #destroy
).
If you want it to raise exceptions instead, you can instruct datamapper to do so either globally, on a per-model, or on a per-instance basis.
1
2
3
DataMapper::Model.raise_on_save_failure = true # globally across all models
Zoo.raise_on_save_failure = true # per-model
zoo.raise_on_save_failure = true # per-instance
If DataMapper is told to raise_on_save_failure
it will raise the following when
any save
operation failed:
You can then go ahead and rescue
from this error.
The example Zoo
To illustrate the various methods used in manipulating records, we’ll create, save, update and destroy a record.
1
2
3
4
5
6
7
8
9
class Zoo
include DataMapper::Resource
property :id, Serial
property :name, String
property :description, Text
property :inception, DateTime
property :open, Boolean, :default => false
end
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.
1
zoo = Zoo.create(:name => 'The Glue Factory', :inception => Time.now)
If the creation was successful, #create
will return the newly created
DataMapper::Resource
. 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. To find out wether the creation
was successful or not, you can call #saved?
on the returned resource. It will
return true
if the resource was successfully persisted, or false
otherwise.
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
.
1
zoo = Zoo.first_or_create(:name => 'The Glue Factory')
This will first try to find a Zoo instance with the given name, and if it fails to do so, it will return a newly created Zoo with that name.
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
.
1
zoo = Zoo.first_or_create({ :name => 'The Glue Factory' }, { :inception => Time.now })
This will search for a Zoo named ‘The Glue Factory’ and if it can’t find one, it will
return a new Zoo instance with its name set to ‘The Glue Factory’ and the inception
set to what has been Time.now at the time of execution. 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. 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.
1
2
3
4
zoo = Zoo.first_or_create({ :name => 'The Glue Factory' }, {
:name => 'Brooklyn Zoo',
:inception => Time.now
})
This will search for a Zoo named ‘The Glue Factory’ but if it fails to find one, it will return a Zoo instance with its name set to ‘Brooklyn Zoo’ and its inception set to the value of Time.now at execution time.
Save
We can also create a new instance of the model, update its properties and then
save it to the data store. The call to #save
will return true
if saving succeeds,
or false
in case something went wrong.
1
2
3
zoo = Zoo.new
zoo.attributes = { :name => 'The Glue Factory', :inception => Time.now }
zoo.save
In this example we’ve updated the attributes using the #attributes=
method,
but there are multiple ways of setting the values of a model’s properties.
1
2
3
zoo = Zoo.new(:name => 'Awesome Town Zoo') # Pass in a hash to the new method
zoo.name = 'Dodgy Town Zoo' # Set individual property
zoo.attributes = { :name => 'No Fun Zoo', :open => false } # Set multiple properties at once
Just like #create
has an accompanying #first_or_create
method, #new
has its
#first_or_new
counterpart as well. The only difference with #first_or_new
is that
it returns a new unsaved resource in case it couldn’t find one for the given query
criteria. Apart from that, #first_or_new
behaves just like #first_or_create
and
accepts the same parameters. For a detailed explanation of the arguments these two methods
accept, have a look at the explanation of #first_or_create
in the above section on Create.
It is important to note that #save
will save the complete loaded object graph when called.
This means that calling #save
on a resource that has relationships of any kind (established
via belongs_to
or has
) will also save those related resources, if they are loaded at the
time #save
is being called. Related resources are loaded if they’ve been accessed either
for read or for write purposes, prior to #save
being called.
NOTE the following behavior of #save
when dm-validations
are in effect!
The symptom that people are seeing is that their records fail to save
(i.e. #save
returns false
) while calling #valid?
returns true
.
This is caused when an object has a parent or child that fails validation
and thus refuses to save, thereby also blocking the object which #save
was called on from saving.
Update
You can also update a model’s properties and save it with one method call.
#update
will return true
if the record saves and false
if the save fails,
exactly like the #save
method.
1
zoo.update(:name => 'Funky Town Municipal Zoo')
One thing to note is that the #update
method refuses to update a resource
in case the resource itself is #dirty?
at this time.
1
2
3
zoo.name = 'Brooklyn Zoo'
zoo.update(:name => 'Funky Town Municipal Zoo')
# => DataMapper::UpdateConflictError: Zoo#update cannot be called on a dirty resource
You can also use #update
to do mass updates on a model. In the previous examples
we’ve used DataMapper::Resource#update
to update a single resource. We can also
use DataMapper::Model#update
which is available as a class method on our models.
Calling it will update all instances of the model with the same values.
1
Zoo.update(:name => 'Funky Town Municipal Zoo')
This will set all Zoo instances’ name property to ‘Funky Town Municipal Zoo’. Internally it does the equivalent of:
1
Zoo.all.update(:name => 'Funky Town Municipal Zoo')
This shows that actually, #update
is also available on any DataMapper::Collection
and performs a mass update on that collection when being called. You typically retrieve
a DataMapper::Collection
from either a call to SomeModel.all
or a call to a
relationship accessor for any 1:n
or m:n
relationship.
Destroy
To destroy a record, you simply call its #destroy
method. It will return
true
or false
depending if the record is successfully deleted or not. Here
is an example of finding an existing record then destroying it.
1
2
zoo = Zoo.get(5)
zoo.destroy # => true
You can also use #destroy
to do mass deletes on a model. In the previous examples
we’ve used DataMapper::Resource#destroy
to destroy a single resource. We can also
use DataMapper::Model#destroy
which is available as a class method on our models.
Calling it will remove all instances of that model from the repository.
1
Zoo.destroy
This will delete all Zoo instances from the repository. Internally it does the equivalent of:
1
Zoo.all.destroy
This shows that actually, #destroy
is also available on any DataMapper::Collection
and performs a mass delete on that collection when being called. You typically retrieve
a DataMapper::Collection
from either a call to SomeModel.all
or a call to a
relationship accessor for any 1:n
or m:n
relationship.
Talking to your datastore directly
Sometimes you may find that you need to execute a non-query task directly against your database. For example, performing bulk inserts might be such a situation.
The following snippet shows how to insert multiple records with only one statement on MySQL. It may not work with other databases but it should give you an idea of how to execute non-query statements against your own database of choice.
1
2
3
4
5
adapter = DataMapper.repository(:default).adapter
# Insert multiple records with one statement (MySQL)
adapter.execute("INSERT INTO zoos (id, name) VALUES (1, 'Lion'), (2, 'Elephant')")
# The interpolated array condition syntax works as well:
adapter.execute('INSERT INTO zoos (id, name) VALUES (?, ?), (?, ?)', 1, 'Lion', 2, 'Elephant')