ufront / ufront-orm

The Object Relational Mapper, allows easy, type-safe access to your database. Builds on Haxe's SPOD but adds macro powered relationships, validation and client side usage
MIT License
13 stars 4 forks source link

ufront-orm

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:

Project Details

Installation:

haxelib install ufront-orm

Using the latest git version:

haxelib git ufront-orm https://github.com/ufront/ufront-orm.git

Current Status:

Contributions:

ufront.db.Object

Extra fields: id, created, modified

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.

The save() method

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.

Client friendly

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.

Other features of ufront.db.Object

Validation

There are 3 ways of adding validation to your ufront models:

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

Relationships

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:

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

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 :)

Differences to Haxe's build in @:relation() metadata

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:

Nullable

If you want it to be optional, so it can be set to null, use Null<BelongsTo<User>>.

HasOne

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).

How we guess the name of the foreign key.

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>;
Nullable

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

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

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:

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:

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.