The ORM (Object Relational Mapper) for Ufront.
This allows you to create a class like this:
import ufront.db.Object;
import ufront.db.ManyToMany;
import sys.db.Types;
class BlogPost extends Object {
var title:SString<255>;
var text:SText;
var author:BelongsTo<User>;
var headerImage:HasOne<ImageAttachment>;
var comments:HasMany<Comment>;
var tags:ManyToMany<BlogPost,Tag>;
}
You can then save these objects to the database easily:
var p = new BlogPost();
p.title = "Short Lambdas";
p.text = "Y U NO short lambdas, Haxe? Seriously, even Java has them.";
p.author = jason;
p.save();
// Look, it has saved, has it's own ID and everything!
trace( p.id, p.created, p.modified );
You can also fetch objects using the manager:
var firstPost = BlogPost.manager.get( 1 );
var allPosts = BlogPost.manager.all();
var postsAboutCats = BlogPost.manager.search($title=="Funny Cat Photo");
trace( postsAboutCats.length ); // How embarrassing!
for ( post in postsAboutCats ) {
post.delete(); // Show no mercy!
}
ufront-orm
builds on Haxe's sys.db.*
DB classes and macros.
For more information on how Haxe Database records work, this wiki page is still the best resource.
This includes information about how the types in sys.db.Types
map to certain database column types.
The key differences in ufront.db.*
compared to sys.db.*
are:
id
, created
and modified
fields built in.save()
method, which calls either insert()
or update()
as required.BelongsTo
, HasOne
, HasMany
and ManyToMany
.Installation:
haxelib install ufront-orm
Using the latest git version:
haxelib git ufront-orm https://github.com/ufront/ufront-orm.git
Current Status:
sys.db.*
classes, which had many bug fixes between Haxe 3.1.3 and 3.2.Contributions:
This extends sys.db.Object as the base class all your models are built upon. It adds 3 fields, which are to be present on every model: id:SId, created:SDateTime, modified:SDateTime.
Forcing an integer unique ID makes it easy for us to work with relationships and generic APIs. I might consider changing this in future so different sorts of primary keys are allowed, or at least, facilitate a way to provide a bigger primary key if you need more than the default Integer size.
The created
and modified
fields are timestamps, and they are updated automatically as you call insert()
, update()
or save()
. This sort of info is used often enough that it's nice to have them as part of the base class, and this is a pattern also seen in other database layers such as ActiveRecord.
We also provide a generic "save()" method. This either inserts or updates an object, and means you don't have to think about whether or not it already exists. The logic goes like this: if your object doesn't have "id" defined, it isn't inserted yet, so call insert(). If it does, it probably already is in the database, so try an "update()", but if that fails, then try "insert()". It should cover most edge cases accurately.
The ufront.db.Object
does a fair amount of conditional compilation to make sure that your models can be seamlessly compiled on the client or on the server. On the client, Object doesn't extend 'sys.db.Object', it has it's own class, so it should compile safely. Now, if you're using the experimental ufront-clientds
haxelib, the client classes can even use save(), delete(), insert() and update(), and they'll start a remoting call and return a promise for when everything is done. Even without ufront-clientds
library though, ufront-orm
will let you build, validate, serialize and unserialize your models on both the client and server, and transfer them using Haxe remoting.
See the SPOD tutorial on the Haxe website for details of how this works.
ufront.db.Object
hxSerialize
and hxUnserialize
methods, and some macro-injected-metadata, to make sure we serialize and unserialize our objects consistently between platforms - which is very helpful for transferring objects between the client and server!public static var manager:Manager<MyModel>;
field on the server.ufront-clientds
haxelib, we create a public static var clientDS:ClientDS<MyModel>;
field on the client.saved
signal that you can use to trigger certain actions after an object has been saved.There are 3 ways of adding validation to your ufront models:
Using @:validate
metadata on a given field.
The first expression in the metadata must be a boolean statement that returns true
if the field is valid.
As a shortcut you can use an _
instead of typing out the field name each time (see password
example below).
You can also give a second argument with text explaining the validation error.
A function validate_myField
function for a specific field.
And field which has a corresponding validate_$fieldName()
function will call that function when performing validation.
The function should perform some validation, and add error messages to the validationErrors
object if the field is invalid.
See the postcode
and validate_postcode
example below.
The class wide function validate():Bool
function.
The validate()
function can be called manually, and is also called when save()
, insert()
or update()
are called.
It checks the validation for every field, and then returns true
or false
.
See the example below for a demonstration of how to include your own logic in the validate()
method.
If you try to save an object which is not valid, a String containing the validation error messages is thrown.
Examples:
@:validate( name!="" )
public var name:SString<50>;
@:validate( _.length>6, "Password must be at least 6 characters long" )
public var password:SString<50>;
@:validate( ~/[a-z0-9_]@mycompany.com/.match(_), "Your email address is not a valid mycompany.com address" )
public var email:Null<SString<50>>;
public var phone:Null<SString<20>>;
public var postcode:SInt;
function validate_postcode() {
var postcodesAvailable = [ 6000, 6001, 6005 ]
if ( postcodesAvailable.indexOf(postcode)==-1 )
validationErrors.set( "postcode", 'Sorry, our service is not available in $postcode yet' );
}
override public function validate():Bool {
super.validate(); // Call all the other validation functions and checks.
if ( phone==null && email==null ) {
validationErrors.set( 'phone', 'Either phone or email must be provided' );
validationErrors.set( 'email', 'Either phone or email must be provided' );
}
return validationErrors.isValid;
}
And then
var u = new AppUser();
u.name = ""; // Fails: "name failed validation."
u.name = "jason"; // Okay
u.password = "test"; // Fails: "Password must be at least 6 characters long"
u.validate(); // True or false
u.validationErrors; // [ name=>"name failed validation", password=>"Password must be at least 6 characters long" ]
u.save(); // Will only save if validate() is true, otherwise will throw an error
Haxe's sys.db.* classes do provide some very basic support for one-to-one relations, but it was relatively inflexible and it required a fair amount of boilerplate code to get other features working, such as many-to-many relationships. I've tried to speed all of that up here with the help of some build macros and a generic "Relationship" class.
There are 4 basic relationships we support so far:
BelongsTo<RelatedModel>
HasOne<RelatedModel>
HasMany<RelatedModel>
ManyToMany<ThisModel,RelatedModel>
Currently I'm not entering foreign keys for these into the database, and DB joins are only used in ManyToMany
, not the other relationship types. So there is room for optimisation here in future.
BelongsTo<T>
specifies a simple, one way relation. Each object of this type, belongs to another one of that type. A Purchase might belong to a Customer, a Photo might belong to a User etc.
The syntax for specifying this is simple:
public var user:BelongsTo<User>;
What this becomes after we do our macro magic:
@:skip @:isVar public var user(get,set):User; // Don't store this column in the database, just store the ID
public var userID:SUId; // A variable for the unique ID representing our related person
// the private getter and setter
function get_user() {
#if server
if (user == null) user = User.manager.get(userID);
#end
return user;
}
function set_user(u:User) {
if ( u==null ) throw 'Field user must not be null';
if ( u.id==null ) throw 'Field user must be set to an object which already has an ID';
userID = u.id;
return user = u;
}
As you can see, it does a fair amount to try and reduce the amount of typing you have to do :)
Haxe has one existing feature for setting up relationships, the @:relation(id)
metadta. In effect, this is almost identical to what we are doing here. Key differences:
BelongsTo<T>
typedef, and don't require metadata.HasOne
and HasMany
fields on the related model.User.manager.serach($group==myGroup)
, but for now we have to do User.manager.search($groupID==myGroup.id)
. I'm hopeful that I can add this feature in future.If you want it to be optional, so it can be set to null, use Null<BelongsTo<User>>
.
HasOne<T>
and BelongsTo<T>
are quite similar, and are related in many ways. The key difference is that the foreign key is stored in the class with the BelongsTo field. Let's look at an example.
In our app, we have a Student model, and a StudentProfile model. Now each Student has exactly one student profile, and each student profile belongs to exactly one student. So which one is BelongsTo, and which one uses HasOne?
In our case, it makes sense that the profile belongs to the student, the student does not belong to their profile. So:
class StudentProfile {
...
public var student:BelongsTo< Student >;
}
class Student {
...
public var studentProfile:HasOne< StudentProfile >;
}
Now, the foreign key will be automatically added to the Student Profile
class StudentProfile {
public var student:BelongsTo< Student >;
public var studentID:SId;
}
The Student
model has no field relating to StudentProfile
in the database. When it needs to get the profile, it's getter will essentially perform something similar to StudentProfile.select($studentID == this.id)
.
For this to work, our build macro has to guess the name of the foreign key in the related table. In the example above, the "profile" getter in the Student model needs to know that in StudentProfile, the foreign key we're looking for is called "studentID". Here we use a convention: by default, we will assume the name is the same as the model name, but with a lower case first letter, and an uppercase "ID" at the end.
So HasOne<Student>
would look for studentID
, and HasOne<StudentProfile>
would look for studentProfileID
.
If that's not what your foreign key is called, say you used child
/childID
instead of student
/studentID
, you can specify this in metadata:
// This tells us, when looking in StudentProfile, our foreign key is "childID", not "studentID"
@:relationKey(childID) public var studentProfile:HasOne<StudentProfile>;
A HasOne<T>
is assumed to be nullable, because you don't know if the Student.manager.select($studentID==this.id)
query will find any results.
HasMany<T>
is used when many related objects belong to this one. So if your comments model has a field:
public var user:BelongsTo<User>;
then you could get your User model to have a HasMany<Comment>
relationship:
public var comments:HasMany<Comment>;
Now, the HasMany<T>
basically translates to List<T>
. But please note that updating the list does not update the database. For example, this doesn't work:
myUser.comments.push(new Comment()); // This would update the list in Haxe, but would not touch the DB
Instead, try this:
var c = new Comment();
c.user = myUser;
c.save(); // As we save this, the next time we retrieve a list of comments for myUser, it will be included.
So that's the basic way this works. Behind the scenes, the build macro basically transforms the code from:
public var comments:HasMany<Comment>;
into:
@:skip public var comments(get,set):Iterable< Comment >;
function get_comments() {
#if server
if (comments == null) Comment.manager.search($userID == this.id)
#end
return comments;
}
function set_comments(comments) {
return this.comments = comments;
}
If no related objects belong to this one, then an empty list will be returned.
ManyToMany<A,B>
is used for situations where many things go together:
// In your Student model
public var classes:ManyToMany<Student,SchoolClass>;
// And the other side, in your SchoolClass model:
public var students:ManyToMany<SchoolClass,Student>;
The first type parameter (A) should be the type of the current class/model, and the second (B) is for the related class/model. These are both fed into a ManyToMany object. The behaviour is a little bit complicated, but it's sort of like this:
_join_${Model1Name}_${Model2Name}
. This table reflects the "Relationship" model.<A>
and <B>
. The relationship merely saves the IDs for A and B to the join table described above. ManyToMany
object, the getter constructs it, and uses an SQL JOIN query to fetch the related B objects using the join table to match them to the current A object.In practice, it looks like this:
// Let's enrol Jason (a student) in a bunch of classes (updating from the student end)
var jason:Student;
jason.classes.setList([scienceClass,englishClass,mathsClass]); // enrol a student in many classes
jason.classes.add(computingClass); // add a single enrolment for this student
// Let's enrol a bunch of students in our science class (updating from the class end)
scienceClass.setList([jason,aaron,anna,justin]); // enrol many students in a class
scienceClass.add(mathilda); // add a single student to this class
// you can also remove things
jason.classes.remove(scienceClass); // remove a single class from this student's enrolments
computingClass.students.clear(); // unenrol all students from this class
// Or iterate over them
for (cl in jason.classes) {
trace ('In ${cl.name}, Jason has ${cl.students.length} class mates');
}
The full list of methods and properties you have access to on a ManyToManyRelation:
length:Int
refreshList()
first():B
add(obj:B)
remove(obj:B)
clear()
setList(iter:Iterable<B>)
iterator():Iterator<B>
See the API documentation for more details.
So in many ways it behaves like a regular list, but it's updating that join table in the background. If there are no related objects, ManyToMany comes back with a length of 0.
Finally, on the client side, the ManyToMany structure survives, but none of the changes are written back to the database. That is to say - if you receive a ManyToMany object through Haxe remoting, it will still be in tact on the other side, but you can't refresh it, add to it, remove from it etc. It's pretty much read-only on the client.