-
Notifications
You must be signed in to change notification settings - Fork 2
The Database (SQL)
If you wish to use a SQL database along with Crails, we warmly recommend you to use the excellent ODB ORM.
Note that our documentation will not teach you how to use ODB. You will need to read and learn the ways of the ODB ORM before using it in a Crails application: have a good look at ODB's manual first.
Apart from integrating ODB within Crails' database system, the crails-odb
module comes with a few tools to integrate odb's compiler within our guard-based pipeline, and to take care of database migrations for you. However, if some of our decisions don't match your needs, you can our guard plugin and our migrating task at anytime, and use odb's tools as you see fit instead.
Once you've installed ODB, following the instructions from its documentation, you will need to compile the crails-odb
module.
If ODB wasn't installed when you first compiled Crails, re-compile it now. It will re-install Crails along with the crails-odb
scripts and libraries.
In your Crails application, use the crails module odb install
command. It will change your CMakeLists.txt
to compile your server with libcrails-odb
, and add the crails-odb
task to your Guardfile
.
You can enable or disable the support for some SQL backends using the ODB_WITH...
options that were added to your CMakeLists.txt
.
When using the ODB ORM, you are supposed to use the odb compiler on your models. The CrailsODB guard plugin takes care of running the odb compiler on all the hpp
files it finds in the folder app/models/
, unless they don't contain any occurences of #pragma db
.
If you are using the CrailsODB guard plugin, then you can also use the odb_migrate
task that comes with the crails-odb module. It is used to apply odb's generated schema on a target database.
The task take the key of your database configuration in config/databases.cpp
as a parameter, to find out which configuration to load. For instance, if you wanted to prepare the "my_sql_db" database configuration, here's the command you'd launch, from your application directory:
build/tasks/odb_migrate/task my_sql_db
If you are not using odb's version pragma, you'll have to drop your database and migrate it again whenever there's a change to your database schema.
Otherwise, odb_migrate
will figure that your database is up to date, and will display "Nothing to do".
If you wish to drop the schema for your database, use the option -d
:
build/tasks/odb_migrate/task -d my_sql_db
The odb_migrate
task also supports odb's version feature, as long as you are using a single version for all your models.
If you wish to implement specific behaviors during a migration, you should implement those in the task/odb_migrate/main.cpp
file, by sending a lambda as a parameter when database.migrate()
is called.
The odb compiler provides many options, some of which you can specify as options of the CrailsODB guard plugin. These options are documented here.
odb compiler options available from CrailsODB guard plugin:
- output (defaults to lib/odb)
- include_prefix (defaults to app/models)
- table_prefix
- std (defaults to c++11)
- default_pointer
- hxx_prologue
- ixx_prologue
- cxx_prologue
- schema_prologue
- generate_session
- at_once
- embed_schema
We also provide our own options:
-
requires
: prepends include directives tohxx_prologue
. -
defines
: provides -D options for theodb
command (defines: ['WITH_DEFINE']
would translate toodb -DWITH_DEFILE
) -
embed_schema
: set to true if you want your databases to be manageable directly from the server, using theODB::Database::migrate
andODB::Database::drop
methods (defaults to false which makes it only available to theodb_migrate
task).
Here's a example of configuration for an application using PostgreSQL, and setting odb's default pointer type to a custom class defined in app/my_ptr_type.hpp
.
# Guardfile
group :before_compile do
guard 'crails-odb', requires: ["app/my_ptr_type.hpp"], default_pointer: "my_ptr_type" do
watch(%r{app/models/.+h(pp|xx)?$})
end
end
Note that crails/safe_ptr.hpp
is always required, so you do not need to specify it if you wish to make Crails' safe_ptr the default pointer type for odb.
From your Crails application, you can get a thread_local
instance of a configured database using the CRAILS_DATABASE
macro (see Databases to know how to configure a database for use with the CRAILS_DATABASE macro).
To use this macro with ODB, you need to include the crails odb database header:
#include <crails/databases/odb.hpp>
The macro will return an instance of ODB::Database
. This object allows you then to get a reference to your database, casting it to the proper database type (that is, odb::pgsql::database
, odb::sqlite::database
, depending on what ODB backend you are currently using).
Here's an example of a route endpoint using ODB to fetch a model:
// app/models/person.hpp
#include <crails/databases/odb.hpp>
#pragma db object pointer(std::shared_ptr)
struct Person
{
friend class odb::access;
#pragma db id auto
unsigned long id;
std::string first_name;
std::string last_name;
};
// app/route_handler.cpp
#include "app/models/person.hpp"
#include "app/models/person-odb.hxx" // don't forget to include the ODB generated headers
using namespace std;
shared_ptr<Person> get_person_from_id(unsigned long id)
{
auto& database = CRAILS_DATABASE(ODB, "my_configuration_name").get_database<odb::pgsql::database>();
return database.query_one<Person>(odb::pgsql::query<Person>::id == person_id);
}
void route_endpoint(Crails::Params& params, std::function<void (DataTree)> callback)
{
DataTree response, response_object;
shared_ptr<Person> person = get_person_from_id(params["id"].as<unsigned long>());
response_object["person"]["first_name"] = person->first_name;
response_object["person"]["last_name"] = person->last_name;
response["body"] = response_object["person"].to_json();
callback(response);
}
The crails-odb module also provides more advanced classes to work with your SQL databases.
The Db::Model
class serves as a basis for your future odb models.
#ifndef PERSON_HPP
# define PERSON_HPP
# include <crails/odb/model.hpp>
# pragma db object pointer(std::shared_ptr)
struct Person : public Db::Model
{
// You do not need to declare an id attribute. Db::Model will handle the id.
// Declare your attributes as you would with any odb object
std::string firstname, lastname;
// Objects inheriting Db::Model will by default be bound to the "default" database.
// If you wish to use another database, you may overload the get_database_name method such as this:
std::string get_database_name() const { return "database_key"; }
// If you intend to use Db::Model with Db::Connection, you MUST provide a Count subclass such as:
#pragma db view pointer(std::shared_ptr) object(Person)
class Count
{
#pragma db column("count(" + Person::id + ")")
size_t value;
};
// The soft-delete behavior is disabled by default. Overload `with_soft_delete` to enable it:
bool with_soft_delete() const { return true; }
// The following hooks are also provided:
void before_save() {}
void after_save() {}
void before_destroy() {}
void after_destroy() {}
};
#endif
The Db::Connection
object handles a transactions for Db::Model
objects.
#include <crails/odb/connection.hpp>
#include "app/models/person.hpp"
#include "app/models/person-odb.hxx"
void route_get_person(Crails::Params& params, std::function<void (DataTree)> callback)
{
Db::Connection database;
DataTree response, response_object;
shared_ptr<Person> person;
database.find_one(person, params["id"].as<Db::id_type>());
response_object["person"]["first_name"] = person->first_name;
response_object["person"]["last_name"] = person->last_name;
response["body"] = response_object["person"].to_json();
callback(response);
}
void route_get_person_index(Crails::Params&, std::function<void (DataTree)> callback)
{
Db::Connection database;
DataTree response, response_object;
odb::result<Person> persons;
database.find<Person>(persons);
for (const Person& person : persons)
{
std::stringstream id;
std::string string_id;
id << person.get_id();
id >> string_id;
response_object[string_id]["first_name"] = person.first_name;
response_object[string_id]["last_name"] = person.last_name;
}
response["body"] = response_object.to_json();
callback(response);
}
void route_add_person(Crails::Params& params, std::function<void (DataTree)> callback)
{
Db::Connection database;
DataTree response, response_object;
Person person;
person.first_name = params["person"]["first_name"];
person.last_name = params["person"]["last_name"];
database.save(person); // will save the object to the database and set the Person's object id
database.commit(); // you must always commit your changes using `Db::Connection::commit`
response_object["person"]["id"] = person.get_id();
response["body"] = response_object["person"].to_json();
callback(response);
}
void route_delete_person(Crails::Params& params, std::function<void (DataTree)> callback)
{
Db::Connection database;
DataTree response, response_object;
shared_ptr<Person> person;
database.find_one(person, params["id"].as<Db::id_type>());
if (person)
{
database.destroy(*person);
database.commit();
}
else
response["status"] = 404;
callback(response);
}
-
Note that there can only be once instance of
Db::Connection
per thread. If such an instance already exists for the current thread, you can access it from anywhere using the global pointer defined asDb::Connection::instance
. -
If you use several databases, note that only one transaction can be opened at the same time, and transactions cannot be shared between databases. As a result, if you interact with a second database, without having committed the changes made to the first database, those changes will be rollbacked.