Pages

Showing posts with label Ruby on Rails. Show all posts
Showing posts with label Ruby on Rails. Show all posts

Sunday, September 28, 2008

Flex Ruby performance with XML, JSON and RubyAMF

One of the great things when you use Flex in combination Ruby is that you can easily change the communication method. For instance you can use xml rpc or json rpc or RubyAMF, just by changing the ruby controller. Off course when you use json then you need to download the corelib for Flex and when you use RubyAMF we need to install this plugin in ruby.
But when you have a choice then you need to know what is the best communication method for your project. In this test I will measure the performance of the three methods. For this I made a simple Flex / Ruby project.

Here are the average results in ms (100 times executed) with the total records count in our test table.
records101004001000
amf159213380731
json4575252781
xml4357197612

Conclusion
RubyAMF has a little overhead and is fast with a large recordset and you can use remoteobject in Flex. Xml is fast but the performance can vary. With 1000 records it can take 200ms but sometimes 2000ms. It looks like Ruby can cache xml and sometimes refresh the cache. Json is very stable and fast with small recordsets. If you just want to retrieve some data in Flex, I would use json but when you want to do more with this data in Flex then RubyAMF has many benefits, like association between objects, conversion to actionscript class and a lot more.

The Ruby Code
you can test the output by adding the format parameter to the url http://localhost:3000/departments/find_all?format=json or format=xml.
Here is the controller code I used

class DepartmentsController < ApplicationController

# return all Departments
def find_all
respond_to do |format|
format.amf { render :amf => Department.find(:all) }
format.json { render :text => Department.find(:all).to_json }
format.xml { render :xml => Department.find(:all) }
end
end

end

department table created in a mysql database

class CreateDepartments < ActiveRecord::Migration

def self.up
create_table :departments do |t|
t.string :name
t.string :location
t.timestamps
end
end

def self.down
drop_table :departments
end
end

RubyAMF configuration

require 'app/configuration'
module RubyAMF
module Configuration
ClassMappings.ignore_fields = ['created_at','updated_at']
ClassMappings.translate_case = true
ClassMappings.assume_types = false
ParameterMappings.scaffolding = false

ClassMappings.register(
:actionscript => 'Department',
:ruby => 'Department',
:type => 'active_record',
:attributes => ["id", "name", "location", "created_at", "updated_at"])

ClassMappings.force_active_record_ids = true
ClassMappings.use_ruby_date_time = false
ClassMappings.use_array_collection = true
ClassMappings.check_for_associations = true
ParameterMappings.always_add_to_params = true
end
end


The Flex code

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
import vo.Timing;

private var idNum:int;
private var maxNum:int = 100;

[Bindable]
private var timing:ArrayCollection = new ArrayCollection();

private function startRubyAMF():void {
timing = new ArrayCollection();
idNum = 0;
loadAll();
}
private function startJSON():void {
timing = new ArrayCollection();
idNum = 0;
loadAllJson();
}
private function startXML():void {
timing = new ArrayCollection();
idNum = 0;
loadAllXML();
}
]]>
</mx:Script>


<mx:Script>
<![CDATA[
import mx.rpc.AsyncToken;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.controls.Alert;
import mx.collections.ArrayCollection;

[Bindable]
private var departments:ArrayCollection = new ArrayCollection();
private var rubyDateFrom:Date;

private function loadAll():void {
rubyDateFrom = new Date();
var token:AsyncToken = AsyncToken(departmentService.find_all());
token.kind = idNum.toString();

var time:Timing = new Timing();
time.id = idNum;
time.startDate = rubyDateFrom;
timing.addItem(time);

}

private function faultHandler(event:FaultEvent):void {
Alert.show(event.fault.faultString + " : " + event.fault.faultCode + " : " + event.fault.faultDetail , "Error in LoginCommand");
}

private function resultHandler(event:ResultEvent):void {
departments = event.result as ArrayCollection;
var rubyDateFinish:Number = new Date().valueOf() - rubyDateFrom.valueOf() ;
rubyLabel.text="RubyAMF departments "+rubyDateFinish.toString()+" ms";

var time:Timing = timing.getItemAt(event.token.kind) as Timing;
time.endDate = new Date();
time.rubyamf = rubyDateFinish;

idNum = idNum +1;
if (idNum < maxNum ) {
loadAll();
} else {
var average:Number = 0;
for ( var i:int = 0 ; i < timing.length ; i++ ) {
var time2:Timing = timing.getItemAt(i) as Timing;
average = average + time2.rubyamf;
}
average = average / timing.length;
rubyLabel.text="RubyAMF departments average "+average.toString()+" ms";

trace("end");
}
}

]]>
</mx:Script>
<mx:HBox>
<mx:Button label="refresh RubyAMF" click="startRubyAMF()"/>
<mx:Button label="refresh JSON" click="startJSON()"/>
<mx:Button label="refresh XML" click="startXML()"/>
</mx:HBox>

<mx:RemoteObject id="departmentService" destination="rubyamf"
endpoint="http://localhost:3000/rubyamf_gateway/"
source="DepartmentsController"
showBusyCursor="true"
result="resultHandler(event)"
fault="faultHandler(event)"
/>

<mx:Label id="rubyLabel" text="RubyAMF departments"/>

<mx:DataGrid id="dg" dataProvider="{departments}">
<mx:columns>
<mx:DataGridColumn dataField="id" headerText="Key"/>
<mx:DataGridColumn dataField="name" headerText="Name"/>
<mx:DataGridColumn dataField="location" headerText="Location"/>
</mx:columns>
</mx:DataGrid>

<mx:Script>
<![CDATA[
import com.adobe.serialization.json.JSON;
[Bindable]
private var dp:ArrayCollection = new ArrayCollection() ;
private var rubyJsonDateFrom:Date;

private function loadAllJson():void {
rubyJsonDateFrom = new Date();
var token:AsyncToken = AsyncToken(json.send());
token.kind = idNum.toString();

var time:Timing = new Timing();
time.id = idNum;
time.startDate = rubyJsonDateFrom;
timing.addItem(time);
}


private function resultHandlerJSON(event:ResultEvent):void
{
dp = new ArrayCollection();
var rawData:String = String(event.result);
var arr:Array = (JSON.decode(rawData) as Array);
for ( var i:int = 0 ; i < arr.length ; i++ ) {
dp.addItem(arr[i].department);
}
var rubyJsonDateFinish:Number = new Date().valueOf() - rubyJsonDateFrom.valueOf() ;
rubyJSONLabel.text="JSON departments "+rubyJsonDateFinish.toString()+" ms";

var time:Timing = timing.getItemAt(event.token.kind) as Timing;
time.endDate = new Date();
time.json = rubyJsonDateFinish;

idNum = idNum +1;
if (idNum < maxNum ) {
loadAllJson();
} else {
var average:Number = 0;
for ( var ii:int = 0 ; ii < timing.length ; ii++ ) {
var time2:Timing = timing.getItemAt(ii) as Timing;
average = average + time2.json;
}
average = average / timing.length;
rubyJSONLabel.text="JSON departments average "+average.toString()+" ms";

trace("end");
}


}
]]>
</mx:Script>

<mx:HTTPService id="json"
url="http://localhost:3000/departments/find_all?format=json"
result="resultHandlerJSON(event)" useProxy="false" />
<mx:Label id="rubyJSONLabel" text="JSON departments"/>
<mx:DataGrid id="dg4" dataProvider="{dp}">
<mx:columns>
<mx:DataGridColumn dataField="id" headerText="Key"/>
<mx:DataGridColumn dataField="name" headerText="Name"/>
<mx:DataGridColumn dataField="location" headerText="Location"/>
</mx:columns>
</mx:DataGrid>

<mx:Script>
<![CDATA[
import com.adobe.serialization.json.JSON;
[Bindable]
private var dp2:ArrayCollection = new ArrayCollection() ;
private var rubyXmlDateFrom:Date;

private function loadAllXML():void {
rubyXmlDateFrom = new Date();
var token:AsyncToken = AsyncToken(xml.send());
token.kind = idNum.toString();

var time:Timing = new Timing();
time.id = idNum;
time.startDate = rubyJsonDateFrom;
timing.addItem(time);


}

private function resultHandlerXML(event:ResultEvent):void
{
if ( event.result != null ) {
dp2 = event.result.departments.department;
}
var rubyXmlDateFinish:Number = new Date().valueOf() - rubyXmlDateFrom.valueOf() ;
rubyXmlLabel.text="XML departments "+rubyXmlDateFinish.toString()+" ms";

var time:Timing = timing.getItemAt(event.token.kind) as Timing;
time.endDate = new Date();
time.xml = rubyXmlDateFinish;

idNum = idNum +1;
if (idNum < maxNum ) {
loadAllXML();
} else {
var average:Number = 0;
for ( var ii:int = 0 ; ii < timing.length ; ii++ ) {
var time2:Timing = timing.getItemAt(ii) as Timing;
average = average + time2.xml;
}
average = average / timing.length;
rubyXmlLabel.text="XML departments average "+average.toString()+" ms";
trace("end");
}
}
]]>
</mx:Script>


<mx:HTTPService id="xml" url="http://localhost:3000/departments/find_all?format=xml"
result="resultHandlerXML(event)" useProxy="false" />
<mx:Label id="rubyXmlLabel" text="XML departments"/>
<mx:DataGrid id="dg5" dataProvider="{dp2}">
<mx:columns>
<mx:DataGridColumn dataField="id" headerText="Key"/>
<mx:DataGridColumn dataField="name" headerText="Name"/>
<mx:DataGridColumn dataField="location" headerText="Location"/>
</mx:columns>
</mx:DataGrid>

</mx:Application>

Monday, September 1, 2008

Associations with Ruby on Rails, Flex and RubyAMF

In this blog item I will you show how easy it is with RoR and RubyAMF to do CRUD operations on tables with associations. RoR and RubyAMF makes it very easy. I used for this blog a simple department and their employees use case.
Here is an picture of the result. A department has employees and an employee belongs to a department

First step is to create the RoR application
rails hr_app

change directory to the new application
cd hr_app

We need to edit the database configuration file
hr_app\config\database.yml

Install RubyAMF
ruby script/plugin install http://rubyamf.googlecode.com/svn/tags/current/rubyamf

Generate the department and employee table
ruby script/generate rubyamf_scaffold department
ruby script/generate rubyamf_scaffold employee


change department table configuration in hr_app/db/migrate folder

class CreateDepartments < ActiveRecord::Migration

def self.up
create_table :departments do |t|
t.string :name
t.string :location
t.timestamps
end
end

def self.down
drop_table :departments
end
end


change employee table also located in the hr_app/db/migrate folder

class CreateEmployees < ActiveRecord::Migration

def self.up
create_table :employees do |t|
t.integer :department_id
t.string :first_name
t.string :last_name
t.string :job
t.timestamps
end
end

def self.down
drop_table :employees
end
end

Let's create the department and employee table
rake db:migrate

We can add one to many associations in RoR
Edit hr_app/app/models/department.rb file where we will add has_many

class Department < ActiveRecord::Base
has_many :employees
end

Edit hr_app/app/models/employee.rb file where we will add belongs_to

class Employee < ActiveRecord::Base
belongs_to :department
end

The RoR configuration is ready we only have to configure RubyAMF

We can generate the class mappings and copy this to the rubyamf_config.rb configuration
ruby script/generate rubyamf_mappings

The generator detects the associations between department and employees
Copy the generated block of text into config/rubyamf_config.rb:
edit hr_app/config/rubyamf_config.rb

require 'app/configuration'
module RubyAMF
module Configuration
ClassMappings.ignore_fields = ['created_at','updated_at']
ClassMappings.translate_case = true
ClassMappings.assume_types = false
ParameterMappings.scaffolding = false

ClassMappings.register(
:actionscript => 'Department',
:ruby => 'Department',
:type => 'active_record',
:associations => ["employees"],
:attributes => ["id", "name", "location", "created_at", "updated_at"])

ClassMappings.register(
:actionscript => 'Employee',
:ruby => 'Employee',
:type => 'active_record',
:associations => ["department"],
:attributes => ["id", "first_name", "last_name", "job", "created_at", "updated_at", "department_id"])

ClassMappings.force_active_record_ids = true
ClassMappings.use_ruby_date_time = false
ClassMappings.use_array_collection = true
ClassMappings.check_for_associations = true
ParameterMappings.always_add_to_params = true
end
end

Now we can start the RoR application
ruby script/server

Create a new Flex application.
First we will create the employee and department actionscript class.
The employee field in the Department class is an arraycollection

package vo
{
import mx.collections.ArrayCollection;


[RemoteClass(alias="Department")]
[Bindable]
public class Department
{
public var id:int;
public var name:String;
public var location:String;
public var createdAt:Date;
public var updatedAt:Date;
public var employees:ArrayCollection;
public function Department()
{
}

}
}

In the Employee class we add the department field of type Department

package vo
{
[RemoteClass(alias="Employee")]
[Bindable]
public class Employee
{
public var id:int;
public var firstName:String;
public var lastName:String;
public var job:String;
public var departmentId:int;
public var createdAt:Date;
public var updatedAt:Date;
public var department:Department;
public function Employee()
{
}
}
}

Here is the mxml code

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="loadAll(); loadAll2();">

<mx:Script>
<![CDATA[
import mx.rpc.AsyncToken;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.controls.Alert;
import vo.Department;
import vo.Employee;
import mx.collections.ArrayCollection;

[Bindable]
private var departments:ArrayCollection = new ArrayCollection();
[Bindable]
private var employees:ArrayCollection = new ArrayCollection();


private function loadAll():void {
var token:AsyncToken = AsyncToken(departmentService.find_all());
token.kind = "fill";
}
private function loadAll2():void {
var token:AsyncToken = AsyncToken(employeeService.find_all());
token.kind = "fill";
}

private function createDept():void {
var dept:Department = new Department();
dept.name = "Headquarters";
dept.location = "Putten";
dept.employees = new ArrayCollection;
var token:AsyncToken = AsyncToken(departmentService.save(dept));
token.kind = "create";
}

private function createDeptWithEmp():void {
var dept:Department = new Department();
dept.name = "Headquarters";
dept.location = "Putten";
dept.employees = new ArrayCollection;

var emp:Employee = new Employee;
emp.firstName = "pipo";
emp.lastName = "pipo";
emp.job = "clown"
dept.employees.addItem(emp);

var token:AsyncToken = AsyncToken(departmentService.save(dept));
token.kind = "create";
}

private function destroy():void {
var token:AsyncToken = AsyncToken(departmentService.destroy(dg.selectedItem.id));
token.kind = "delete";
}

private function faultHandler(event:FaultEvent):void {
Alert.show(event.fault.faultString + " : " + event.fault.faultCode + " : " + event.fault.faultDetail , "Error in LoginCommand");
}

private function resultHandler(event:ResultEvent):void {
if ( event.token.kind == "fill" ) {
departments = event.result as ArrayCollection;
} else {
loadAll();
loadAll2();
}
}
private function resultHandler2(event:ResultEvent):void {
if ( event.token.kind == "fill" ) {
employees = event.result as ArrayCollection;
} else {
loadAll2();
}
}
]]>
</mx:Script>


<mx:RemoteObject id="departmentService" destination="rubyamf"
endpoint="http://localhost:3000/rubyamf_gateway/"
source="DepartmentsController"
showBusyCursor="true"
result="resultHandler(event)"
fault="faultHandler(event)"
/>

<mx:RemoteObject id="employeeService" destination="rubyamf"
endpoint="http://localhost:3000/rubyamf_gateway/"
source="EmployeesController"
showBusyCursor="true"
result="resultHandler2(event)"
fault="faultHandler(event)" />

<mx:HBox>
<mx:VBox>
<mx:Label text="Departments with employees"/>
<mx:HBox>
<mx:Button label="Create Department" click="createDept()"/>
<mx:Button label="Create Department with Employee" click="createDeptWithEmp()"/>
<mx:Button label="Remove Department" click="destroy()"/>
</mx:HBox>
<mx:DataGrid id="dg" dataProvider="{departments}">
<mx:columns>
<mx:DataGridColumn dataField="id" headerText="Key"/>
<mx:DataGridColumn dataField="name" headerText="Name"/>
<mx:DataGridColumn dataField="location" headerText="Location"/>
</mx:columns>
</mx:DataGrid>

<mx:DataGrid id="dg_2" dataProvider="{dg.selectedItem.employees}">
<mx:columns>
<mx:DataGridColumn dataField="id" headerText="Key"/>
<mx:DataGridColumn dataField="firstName" headerText="First Name"/>
<mx:DataGridColumn dataField="lastName" headerText="Last Name"/>
</mx:columns>
</mx:DataGrid>

</mx:VBox>
<mx:VBox>

<mx:Label text="Employees"/>

<mx:DataGrid id="dg2" dataProvider="{employees}" variableRowHeight="true">
<mx:columns>
<mx:DataGridColumn dataField="id" headerText="Key"/>
<mx:DataGridColumn dataField="firstName" headerText="First Name"/>
<mx:DataGridColumn dataField="lastName" headerText="Last Name"/>
<mx:DataGridColumn dataField="department">
<mx:itemRenderer>
<mx:Component>
<mx:VBox>
<mx:Text text="{data.department.name}"/>
<mx:Text text="{data.department.location}"/>
</mx:VBox>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
</mx:columns>
</mx:DataGrid>
</mx:VBox>

</mx:HBox>
</mx:Application>

That's all. you can easily create and retrieve departments with it's employees with RoR and RubyAMF. RoR and RubyAMF do all the hard work.

Friday, August 29, 2008

Flex and Ruby on Rails with RubyAMF

My earlier Flex blogs often talks about Flex in combination with java but you don't need to java you can use Ruby on Rails too. There are a lot blog entries how to do this. I already made a little demo application where I use RoR and the HttpService in Flex. This solution is based on REST Web Services. RoR gives xml as output.
In this Blog entry I will use RubyAMF. With RubyAMF I can use RemoteObject, RubyAMF will convert the Ruby datatypes to the Flex datatypes and it is really fast.
If you want more information on RubyAMF just click here or go to the RubyAMF discussion groups

Let's create a new Flex Ruby application where we will use RubyAMF. I will create a small comic entry application.

Create a new RoR application:
rails comics_catalog

Change directory to the new comics_catalog folder:
cd comics_catalog

Edit the database connection file ( comics_catalog\config\database.yml ) with the right connection.

Update the project with RubyAMF:
ruby script/plugin install http://rubyamf.googlecode.com/svn/tags/current/rubyamf
This will download RubyAMF code and configuration from subversion to your project.

Create the comic table and controller
ruby script/generate rubyamf_scaffold comic

Edit the comic table configuration. Go to comics_catalog\db\migrate\ folder and edit the 20080827214400_create_comics.rb file. You will have an other filename because of the timestamp. The script need to look like this.

class CreateComics < ActiveRecord::Migration
def self.up
create_table :comics do |t|
t.string :name
t.string :description
t.float :price
t.timestamps
end
end

def self.down
drop_table :comics
end
end

Let's create the comic table in the database
rake db:migrate

The last step in RoR is to configure RubyAMF. For this we need to go the comics_catalog\config folder where we will edit the rubyamf_config.rb file.

require 'app/configuration'
module RubyAMF
module Configuration
ClassMappings.ignore_fields = ['created_at','created_on','updated_at','updated_on']
ClassMappings.translate_case = true
ClassMappings.assume_types = false
ParameterMappings.scaffolding = false


ClassMappings.register(:actionscript => 'Comic',
:ruby => 'Comic',
:type => 'active_record',
:attributes => ["id","name","description","price", "created_at", "updated_at"])

ClassMappings.force_active_record_ids = true
ClassMappings.use_ruby_date_time = false
ClassMappings.use_array_collection = false
ClassMappings.check_for_associations = false
ParameterMappings.always_add_to_params = true
end
end

I use ClassMappings.ignore_fields because I want to ignore the automatic created fields Ruby will fill these field automatically.
ClassMappings.translate_case is important if you want to Java naming style. RubyAMF will convert created_at to createdAt.
ClassMappings.assume_types is important if you do your know class mapping. In our case we will create the comic class in Flex too so we don't need this. This will give us a little more performance.
ParameterMappings.scaffolding is handy if you just want to pass an comic object or id to the delete method else you need to use {id:dg.selecteditem.id}
ClassMappings.register(:actionscript => 'Comic'. Don't use the package name just put in the actionscript class name.

Start the Ruby server
ruby script/server

Now we can go to Flex, where we create an new application.

first create a comic actionscript object

package vo
{

[RemoteClass(alias="Comic")]
[Bindable]
public class Comic
{
public var id:int;
public var name:String;
public var description:String;
public var price:Number;
public var createdAt:Date;
public var updatedAt:Date;
public function Comic()
{
}


}
}

And here is the mxml code.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="loadAll2();">

<mx:Script>
<![CDATA[
import mx.rpc.AsyncToken;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.controls.Alert;
import vo.Comic;

[Bindable]
private var comics:Array = new Array();

private function loadAll2():void {
var token:AsyncToken = AsyncToken(comicService.find_all());
token.kind = "fill";
}

private function save2():void {
var comic:Comic = new Comic();
comic.id = dg2.selectedItem.id;
comic.name = Name2.text;
comic.description = Description2.text;
var token:AsyncToken = AsyncToken(comicService.save(comic));
token.kind = "save";

}

private function create2():void {
var comic:Comic = new Comic();
comic.name = Name2.text;
comic.description = Description2.text;
var token:AsyncToken = AsyncToken(comicService.save(comic));
token.kind = "create";
}


private function destroy2():void {
var token:AsyncToken = AsyncToken(comicService.destroy(dg2.selectedItem.id));
token.kind = "delete";
}

private function faultHandler(event:FaultEvent):void {
Alert.show(event.fault.faultString + " : " + event.fault.faultCode + " : " + event.fault.faultDetail , "Error in LoginCommand");
}

private function resultHandler2(event:ResultEvent):void {
if ( event.token.kind == "fill" ) {
comics = event.result as Array;
} else {
loadAll2();
}
}



]]>
</mx:Script>
<mx:RemoteObject id="comicService" destination="rubyamf"
endpoint="http://localhost:3000/rubyamf_gateway/"
source="ComicsController"
showBusyCursor="true"
result="resultHandler2(event)"
fault="faultHandler(event)" />

<mx:ApplicationControlBar>
<mx:Button label="Create" click="create2()"/>
<mx:Button label="Update" click="save2()"/>
<mx:Button label="Delete" click="destroy2()"/>
<mx:Button label="Refresh" click="loadAll2()"/>
</mx:ApplicationControlBar>

<mx:DataGrid id="dg2" dataProvider="{comics}">
<mx:columns>
<mx:DataGridColumn dataField="id" headerText="Key"/>
<mx:DataGridColumn dataField="name" headerText="Name"/>
<mx:DataGridColumn dataField="description" headerText="Description"/>
</mx:columns>
</mx:DataGrid>
<mx:Form >
<mx:FormItem label="Name">
<mx:TextInput id="Name2" text="{dg2.selectedItem.name}" />
</mx:FormItem>
<mx:FormItem label="Description">
<mx:TextInput id="Description2" text="{dg2.selectedItem.description}"/>
</mx:FormItem>
</mx:Form>
</mx:Application>


That's All