Created by hww3. Last updated by hww3,
8 years ago. Version #16.
Fins includes a powerful object relational mapping (ORM) framework for use in developing the data model domain of your application. Basically, the Fins.Model framework allows you to wrap a SQL database with a set of Pike objects. This means that you can write an entire data driven application without writing a single SQL statement.
Additionally, with the data of your application represented as objects, you can add functionality to your classes that operate with the object's data. This enables application developers to further separate your data domain's business logic.
The Fins.Model framework consists of a set of classes and levels of functionality that you can mix and match in whatever ways fit your application's needs.
take a look at this beginning to end demo of how to get the model up and running with some objects: ModelDemo
The Model Context
- Model Context
- Data Object Definition
- Direct Access Implementation
- Special "field types"
- Multiple Models
is a class that defines a central point for working with the object types used by your data model. For instance, this class contains the repository object which contains all of your model's data mapping objects as well as the methods required to manually register new data object types. The context maintains the connection to your model's database and includes functionality for searching and creating new instances of data model objects as well as initiating transactions. For high volume applications, additional connections to the database can be opened by cloning the context object. For each model defined in your application's configuration file, a context object will be created which can be used to interact with that model.
You can retrieve a DataModelContext from the Fins.DataSource module:
// if we have only one model defined, it's always available as _default:
Fins.Model.DataModelContext c = Fins.DataSource._default;
// or if we've defined a model id in the configuration file, by name:
Fins.Model.DataModelContext d = Fins.DataSource.myModel;
For each data object type your application will use, one or two classes must be defined. The first class is mandatory, and should be a descendant of Fins.Model.DataObject
, which contains the definition of the table and all fields contained in the data type. There is one instance of this class for each application. When you access your data, this "master" data object will provide you with an instance of DataObjectInstance
that is keyed to the given record in the database. This definition may be provided manually using add_field() and friends, created dynamically from the database schema (see Model Reflection
), or a combination of the two.
You may optionally create a custom class that inherits Fins.Model.DirectAccessInstance
, in which you can provide custom data domain functions that will be able to work with a given row of data in the database. This class, if you provide it, will allow you to create new objects (and thus records in the database) by simply creating a new instance of the class. Note that this method of object creation is optional; you can also use the new()
method in your model context interchangably.
// a data type definition object, we'll call it App.Model.Foo.pike
// aka MyAppDir/modules/App.pmod/DataMappings.pmod/Foo.pike
// if we don't define the define() method, Fins will attempt to configure the
// data object by looking at the definition of the table in the database.
// a set of rules will be used to determine the way in which the fields from
// the table will be mapped.
// a direct access definition object, we'll call it App.Objects.Foo.pike
// aka MyAppDir/modules/App.pmod/Objects.pmod/Foo.pike
string type_name = "Foo";
object repository = App.Repo;
In order to set up a working model, once you have created the DataObject
subclasses and (optionally) created DataObjectInstance
subclasses, you'll make sure these classes are registered. There are two ways to accomplish this. The first is to implement the register_types()
method in your application's model class. This implementation should contain a call to repository->register_type()
for each data type in your model. This technique can be useful if your datatype names are unusually laid out.
The second option is to allow Fins to look at the code in your application to automatically register your data types. To do this, you must put your data type definitions in one module, called AppName.DataMappings
, and the direct access instance definitions (if they exist,) in another, called AppName.Objects
. The Fins application loader (Fins.Loader
) will look to see if these modules exist, and configure the model appropriately. When using pike -x fins create
, these modules will be created automatically for you.
Name your classes according to the rules of Model Reflection: they should be singularized versions of the tablenames. For example, an object that represents the table "users" would be named "User". The classes in the two modules should be similarly named in order for the automatic registration to associate the two.
A recently added tool has been added that eases the effort required in getting a working model. The tool, invoked with pike -x fins model
creates the stub classes necessary (as described above) for your model to link up with a database table. If you're using manual datatype registration, you'll need to add the appropriate lines to your model class' register_types()
pike -x fins model AppDir table1 [… tableN]
The Model framework is fairly flexible outside of a few absolute requirements. However, the more more closely you follow accepted convention, the less work you will need to do in order to define your Model objects. More details about this will follow, but first, let's discuss the unchangable requirements:
1. Each table that represents a datatype (ie any table that isn't a join table) must have a unique identifier. For our purposes, this must be an auto-incrementing integer. Most databases support this type of field in one way or another. The three current targets (Mysql, Postgres and SQLite) all support this in a relatively simple way. Oracle is known for being trickier.
2. The converse of unchangable requirement #1 is that we don't support tables that have multi-field keys. It's just not supported at this point.
3. We assume the id field is immutable once assigned.
That's pretty much it. Everything else can be worked around in one way or another.
Now, in order for automatic type definition (see Model Reflection
) to work, you need to follow some conventions. If you don't follow conventions, you'll either need to supplement the basic definitions (such as for foreign keys and multi-row joins). If you're saddled with a legacy database that otherwise meets the unchangable requirements listed above, it probably means that you'll need to define your datatype mappings manually. Of course, this is tedious, but far from the end of the world, as it's not something you're likely to change on a daily basis.
As we've just learned, Fins dictates the primary key of any datatype table. Sometimes this is inconvenient, so Fins allows you to specify an "alternate" key. This allows you to access objects by the value in the alternate key rather than the id field. Methods in your Repo object provide access to objects in this manner.
To set an alternate key, add the following line to the post_define()
method in your Datatype definition class (myappname
Note that finsfieldname
is the string you'd use to access that field in fins ( ie, someobject[ create "finsfieldname"
], which may not be the name of the field in the database).
Additionally, datatypes that have alternate keys defined will display that value when printing the object using sprintf("%O", ...)
Currently, alternate keys have no other bearing on functionality (that is, they don't enforce uniqueness).
Using the Model
If you've taken the easy path (and hopefully you have!), Fins will provide some convenience functions for working with the model. We'll go over each of the major CRUD functions in turn.
Creating New Objects
You can either use DataModelContext.new("objecttype") or Fins.Object.ObjectType() to get a new object instance:
object dmc = Fins.DataSource._default;
object u = dmc->new("user");
object u = MyApp.Objects.User();
u["username"] = "john doe";
object dmc = Fins.DataSource._default;
object u = dmc->find->users_by_id(1);
array users = dmc->find->users_all();
array users = dmc->find->users((["somefield": "somevalue"]));
// if we've defined an alternate id field, we'll also get
object u = dmc->find->user_by_alternate("alternate value");