Using an ORM makes your code look a little cleaner. Today we can take a look at Xoos, an ORM aimed at making your life simpler, your code cleaner, and not getting in the way of your development.
This article will cover implementing an ERD in your code and show example uses.
Key features of Xoos:
- flexible configuration (YAML files as well as classes, or just one or the other)
- relational modeling made easy
- convenience methods on both rows & models
- validation
quick terminology
model
You can think of this in database terms as a table but in the rest of the article a model
is meant to mean the class+{convenience methods}+{optional yaml}
If you're coming from DBIx this can be thought of the same as a resultclass.
row
Think of this in terms of the row class+{convenience methods}+{columns inherited from the model}
set up
Using zef we'll install Xoos (if YAML::Parser::LibYAML fails to install or you don't want to install libyaml, then don't fret - you don't need it and it's just in the article to demonstrate YAML models).
zef install DB::Xoos YAML::Parser::LibYAML
SQLite3, we'll create the following ERD.
example$ sqlite3 xoos.sqlite3
SQLite version 3.19.3 2017-06-27 16:48:08
Enter ".help" for usage hints.
sqlite> create table books (book_id integer primary key autoincrement, title varchar(64) not null, author_id integer not null, date_published date, price float not null);
sqlite> create table authors (author_id integer primary key autoincrement, name varchar(64) not null, birth_date date);
sqlite> create table customers (customer_id integer primary key autoincrement, email_address varchar(64) not null, name varchar(64) not null, date_registered date default (datetime('now','localtime')));
sqlite> create table orders (order_id integer primary key autoincrement, customer_id integer not null, order_date date default (datetime('now','localtime')));
sqlite> create table order_details (order_detail_id integer primary key autoincrement, order_id integer not null, book_id integer not null);
sqlite> .quit
That's it for setup. Now we can start using Xoos and implementing our application.
commence coding!
This example is going to contain a lib
directory and a bunch of other scripts for doing useful things with our models/rows.
ORM setup
Now we must create our ORM classes.
We'll develop two of the models, one with standard perl6 and one using YAML, the rest of ERD can be implemented as a practice or the entire project can be downloaded at the end of the article.
App::Model::Book
In our path we're going to use the prefix of App
for our models and rows. If you haven't already and you're following along, then create the directory lib/App/Model
and then edit the file lib/App/Model/Book.pm6
.
use DB::Xoos::Model;
unit class App::Model::Book does DB::Xoos::Model['books'];
qw<...>
Here we are simply defining our class and telling DB::Xoos to use the table books
as our source of data for this model. Next we'll let the ORM know what columns are available.
has @.columns = [
book_id => {
type => 'integer',
is-primary-key => True,
auto-increment => True,
},
title => {
type => 'varchar',
nullable => False,
length => '64',
},
author_id => {
type => 'varchar',
nullable => False,
length => '64',
},
date_published => {
type => 'date',
},
price => {
type => 'float',
},
];
Fairly straight forward. Using is-primary-key
on multiple columns will allow you to key off of multiple columns, creating a compounded key.
has @.relations = [
authors => {
:has-one,
:model<Author>,
:relate(author_id => 'author_id'),
},
order_details => {
:has-many,
:model<OrderDetails>,
:relate(book_id => 'book_id'),
},
];
Here we're defining the relationships that models have with other models. We have a 1:N with :has-many
and a 1:1 with :has-one
.
After that our model is defined and if we have no row
methods we would like to implement then we're done.
App::Model::OrderDetail
If you haven't already and you're following along, then create and edit the file models/orderdetail.yaml
.
table: order_details
name: OrderDetail
columns:
order_detail_id:
type: integer
nullable: false
is-primary-key: true
auto-increment: true
order_id:
type: integer
nullable: false
book_id:
type: integer
nullable: false
relations:
book:
has-one: true
model: Book
relate:
book_id: book_id
order:
has-one: true
model: Order
relate:
order_id: order_id
As you can see this is essentially doing what we did above without necessitating the explicit creation of the entire class.
Note: using yaml doesn't preclude you from putting model methods in a class App::Model::OrderDetail but it can make your model class methods look cleaner!
App::Model::*
The rest of the model creations would be redundant so, the rest of the exercise is left as just that (or it's available at the end of the article).
That's it! Your models are ready to be used, the rest of the code is going to be in scripts placed in the bin/
folder.
Example Usage
First ORM usage
We'll list what models we have available to us from ORM, this script is called show-models
. All of the following examples will assume you've done the same code up to and including the .connect
and will be omitted for brevity.
use DB::Xoos::SQLite;
my $xoos = DB::Xoos::SQLite.new;
$xoos.connect('sqlite://xoos.sqlite3', :options({
:prefix<App>, # loads classes from App::Model::*
model-dirs => [qw<models>], # for our yaml column definitions
}));
"Successfully loaded models:\n {$xoos.loaded-models.join("\n ")}".say;
This script should output something like:
Successfully loaded models:
Customer
Author
OrderDetail
Order
Books
While not entirely useful, it does let us know that everything loaded well. Now let's fill up our tables.
Loading data into tables from CSV
This script will use Text::CSV
and load data from data/books.csv
and generate other made up data. This is a truncated version because it's mostly long and repititous for our use. Generating customers, orders, and order details is left to the user or, again, you can find a full script in the file at the end of the article.
qw<snipped setup for Xoos>;
use Text::CSV;
my @rows = csv(:in('data/books.csv'));
my $book = $xoos.model('Book');
my $auth = $xoos.model('Author');
# generate book and author data from csv
for @rows[1..*] -> $row {
# determine whether an author by that name exists, this isn't
# robust as there may be many authors by the same name
my $author = $auth.search({
name => $row[1] eq '' ?? 'unknown' !! $row[1];
});
# if an author was found, use it
if $author.count {
$author .=first;
} else {
$author = $auth.new-row;
$author.name($row[1] eq '' ?? 'unknown' !! $row[1]);
$author.update;
}
# create our book and generate a random price
$book.new-row({
author_id => $author.author_id,
title => $row[0],
price => (299 .. 5000).pick(1)[0] / 100,
}).update;
}
Viewing or finding customer information
In this example we'll be using a relationship on customer to get the number of orders and how many items each order contained. The script also uses a convenience method built upon the App::Row::Customer
to show how many total items they've ordered.
use DB::Xoos::SQLite;
my DB::Xoos::SQLite $db .=new;
multi MAIN(:$email?, :$name, :$customer-id) {
$db.connect('sqlite://xoos.sqlite3', :options({
model-dirs => [qw<models>],
prefix => 'App',
}));
my %search;
my $customers = $db.model('Customer');
%search<customer_id> = $customer-id if $customer-id.defined;
%search<name> = %(
'like' => '%' ~ $name ~ '%',
) if $name.defined;
%search<email_address> = %(
'like' => '%' ~ $email ~ '%',
) if $email.defined;
if %search.keys {
$customers .=search(%search);
say "Searching with params:";
dd %search;
}
say 'Customer info:';
printf "| %-14s | %-17s | %-14s | %-12s | %-14s |\n", qw<customer_id email name orders order-items>;
say 'x' x 87;
for $customers.all -> $customer {
printf "| %-14s | %-17s | %-14s | %-12s | %-14s |\n",
$customer.customer_id,
$customer.email_address,
$customer.name,
$customer.orders.count,
$customer.total-order-items; # this uses the row level convenience method in App::Row::Customer
}
say 'x' x 87;
}
If you have questions or would like to see more, exploring this or other topics, please ping me @tony-o in #perl6 on freenode.
Here is a link to files for this guide github repo, zip file