Upgrade to Pro — share decks privately, control downloads, hide ads and more …

LazyRecord: The Fast PHP ORM

LazyRecord: The Fast PHP ORM

Yo-An Lin

April 22, 2012
Tweet

More Decks by Yo-An Lin

Other Decks in Programming

Transcript

  1. .metadata • 林佑安 (c9s) • 190 repo/projects on GitHub •

    Perl programming since 2008 • PHP programming since last year • c, c++, javascript, obj-c, ruby, python, haskell, java, c#, VB .NET ... 12年4月22⽇日星期⽇日
  2. Propel / Doctrine • Propel uses XML Schema file. •

    Doctrine uses XML/YAML/Annotations. 12年4月22⽇日星期⽇日
  3. Propel / Doctrine • Propel uses XML Schema file. •

    Doctrine uses XML/YAML/Annotations. • Slow & Fat. 12年4月22⽇日星期⽇日
  4. Propel / Doctrine • Propel uses XML Schema file. •

    Doctrine uses XML/YAML/Annotations. • Slow & Fat. • Doctrine is too complicated. 12年4月22⽇日星期⽇日
  5. • XML for configuration file. • XML for schema file.

    • XML for everything. • Concepts are from Java, too complicated. 12年4月22⽇日星期⽇日
  6. <?xml version="1.0"?> <config> <log> <ident>propel-bookstore</ident> <type>console</type> <level>7</level> </log> <propel> <datasources

    default="bookstore"> <datasource id="bookstore"> <adapter>sqlite</adapter> <connection> <classname>DebugPDO</classname> <dsn>mysql:host=localhost;dbname=bookstore</dsn> <user>testuser</user> <password>password</password> <options> <option id="ATTR_PERSISTENT">false</option> </options> <attributes> <option id="ATTR_EMULATE_PREPARES">true</option> </attributes> <settings> <setting id="charset">utf8</setting> <setting id="queries"> <query>set search_path myschema, public</query><!-- automatically set postgresql's search_path --> <query>INSERT INTO BAR ('hey', 'there')</query><!-- execute some other query --> </setting> </settings> </connection> <slaves> <connection> <dsn>mysql:host=slave-server1; dbname=bookstore</dsn> </connection> <connection> <dsn>mysql:host=slave-server2; dbname=bookstore</dsn> </connection> </slaves> </datasource> </datasources> <debugpdo> <logging> <details> <method> <enabled>true</enabled> </method> <time> <enabled>true</enabled> <precision>3</precision> </time> <mem> <enabled>true</enabled> <precision>1</precision> </mem> 12年4月22⽇日星期⽇日
  7. <datasource id="bookstore"> <adapter>sqlite</adapter> <connection> <classname>DebugPDO</classname> <dsn>mysql:host=localhost;dbname=bookstore</dsn> <user>testuser</user> <password>password</password> <options> <option

    id="ATTR_PERSISTENT">false</option> </options> <attributes> <option id="ATTR_EMULATE_PREPARES">true</option> </attributes> <settings> <setting id="charset">utf8</setting> <setting id="queries"> <query>set search_path myschema, public</query><!-- automatically set postgresql's search_path --> <query>INSERT INTO BAR ('hey', 'there')</query><!-- execute some other query --> </setting> </settings> </connection> <slaves> <connection> <dsn>mysql:host=slave-server1; dbname=bookstore</dsn> </connection> <connection> <dsn>mysql:host=slave-server2; dbname=bookstore</dsn> </connection> </slaves> </datasource> </datasources> <debugpdo> <logging> <details> <method> <enabled>true</enabled> </method> <time> <enabled>true</enabled> <precision>3</precision> </time> <mem> <enabled>true</enabled> <precision>1</precision> </mem> </details> </logging> </debugpdo> </propel> </config> 12年4月22⽇日星期⽇日
  8. Inspirations • JiftyDBI / Perl • KiokuDB / Perl •

    ActiveRecord / Ruby • Propel / PHP 12年4月22⽇日星期⽇日
  9. client = Client.find(10) client = Client.first Client.where("orders_count = ?", params[:orders])

    Client.where("created_at >= :start_date AND created_at <= :end_date", {:start_date => params[:start_date], :end_date => params[:end_date]}) Client.order("created_at DESC") Client.limit(5).offset(30) 12年4月22⽇日星期⽇日
  10. Object::Declare Audrey Tang 唐鳳 use Object::Declare ['MyApp::Column', 'MyApp::Param']; my %objects

    = declare { param foo => !is global, is immutable, valid_values are qw( more values ); column bar => field1 is 'value', field2 is 'some_other_value', sub_params are param( is happy ), param ( is sad ); }; print $objects{foo}; # a MyApp::Param object print $objects{bar}; # a MyApp::Column object # Assuming that MyApp::Column::new simply blesses into a hash... print $objects{bar}{sub_params}[0]; # a MyApp::Param object print $objects{bar}{sub_params}[1]; # a MyApp::Param object 2006 12年4月22⽇日星期⽇日
  11. Jifty::DBI package Simple; use Jifty::DBI::Schema; use Jifty::DBI::Record schema { column

    foo => type is 'text'; column bar => type is 'text'; }; 12年4月22⽇日星期⽇日
  12. Jifty::DBI package TestApp::Model::Phone; use Jifty::DBI::Schema; use Jifty::DBI::Record schema { column

    user => references TestApp::Model::User by 'id', is mandatory; column type => ...; column value => validator is sub { ... }, default is sub { } ; }; 12年4月22⽇日星期⽇日
  13. What we need • Can use PHP closures for validation,

    default value, completion ..etc. • Everything should be lazy. • Simple API • No overdesign. • Mixin schema • CRUD generation. • Front-end CRUD integration. 12年4月22⽇日星期⽇日
  14. PHP 5.3 Characteristic • APC is fast. • require /

    include is slow. • json_encode / json_decode are slower than require. • method is slower than properties. • magic method is slower than normal method. • array is always faster than object. 12年4月22⽇日星期⽇日
  15. EteDB • Initialize model schema in runtime. • Schema is

    defined in Model (in __constructor). • MySQL only. • dynamic class generator (using eval) • too slow. 12年4月22⽇日星期⽇日
  16. LazyRecord • Lazy schema loader • Lazy attribute • Lazy

    class loader • Lazy connection • Static class generator • SQL Generator for MySQL, PgSQL, SQLite • SplClassLoader • ... etc 12年4月22⽇日星期⽇日
  17. SQLBuilder • A Simple SQL Generator. • Prevent Injection. •

    Migration generator. (index, alter table...etc) • Support SQLite, Pgsql, Mysql syntax. • Pure SQL or with named-parameters. 12年4月22⽇日星期⽇日
  18. <?php $sqlbuilder = new SQLBuilder\QueryBuilder( $driver ); $sql = $sqlbuilder->table('authors')->insert([

    'name' => 'Mary', 'address' => 'Paris', ])->build(); 12年4月22⽇日星期⽇日
  19. -- General syntax INSERT INTO authors ( name , address

    ) VALUES ( 'Name' , 'Address' ); -- PgSQL INSERT INTO "Authors" ( "Name" , "Address" ) VALUES ( 'Name' , 'Address' ); -- MySQL INSERT INTO `Authors` ( `Name` , `Address` ) VALUES ( 'Name' , 'Address' ); -- PDO INSERT INTO authors ( name , address ) VALUES ( ? , ? ); INSERT INTO authors ( name , address ) VALUES ( :name , :address ); 12年4月22⽇日星期⽇日
  20. <?php $sql = $builder->table('Member')->select('*') ->where() ->equal( 'a' , 'bar' )

    // a = 'bar' ->notEqual( 'a' , 'bar' ) // a != 'bar' ->is( 'a' , 'null' ) // a is null ->isNot( 'a' , 'null' ) // a is not equal ->greater( 'a' , '2011-01-01' ); ->greater( 'a' , ['date(2011-01-01)'] ); // do not escape ->or()->less( 'a' , 123 ) ->and()->like( 'content' , '%content%' ); ->group() // and ( a = 123 or b != 123 ) ->is( 'a' , 123 ) ->isNot( 'b', 123 ) ->ungroup() ->build(); 12年4月22⽇日星期⽇日
  21. <?php $author = new Author; $ret = $author->create([ 'name' =>

    "Deflator Test $i", 'country' => 'Tokyo', 'confirmed' => true, 'date' => new DateTime('2011-01-01 00:00:00'), ]); if( $ret->success ) { echo "Created!"; } 12年4月22⽇日星期⽇日
  22. <?php $ret = $author->update(array( 'name' => 'Bar' )); if( $ret->success

    ) { echo "Updated!"; } else { echo $ret; // __toString support } 12年4月22⽇日星期⽇日
  23. <?php $record = Author::load(array( 'name' => 'Foo' )); // To

    find a record with primary key: $record = Author::load( 1 ); // To update a record (static): $ret = Author::update( array( 'name' => 'Author' ))->where() ->equal('id',3) ->execute(); 12年4月22⽇日星期⽇日
  24. <?php $authors = new AuthorCollection; foreach( $authors as $author )

    { echo $author->name , "\n" } Iterator 12年4月22⽇日星期⽇日
  25. <?php $newCollection = $names->filter(function($item) { // do something else })->filter(function($item)

    { return $item->confirmed; }); ?> Filter 12年4月22⽇日星期⽇日
  26. <?php /* page 1, 10 per page */ $authors =

    new AuthorCollection; $pager = $authors->pager(1,10); $pager = $authors->pager(); $items = $pager->items(); $pager->next(); // next page ?> Integrate with OFFSET & LIMIT Collection Pager 12年4月22⽇日星期⽇日
  27. Relationship <?php // has many $address = $author->addresses->create([ 'address' =>

    'farfaraway' ]); // create related address $author->addresses[] = [ 'address' => 'Harvard' ]; $addresses = $author->addresses->items(); foreach( $author->addresses as $address ) { echo $address->address , "\n"; } 12年4月22⽇日星期⽇日
  28. <?php use LazyRecord\Schema\SchemaDeclare; class AddressSchema extends SchemaDeclare { function schema()

    { $this->column('address') ->varchar(128); } } 12年4月22⽇日星期⽇日
  29. <?php use LazyRecord\Schema\SchemaDeclare; class AddressSchema extends SchemaDeclare { function schema()

    { $this->column('address') ->integer(); } } 12年4月22⽇日星期⽇日
  30. <?php use LazyRecord\Schema\SchemaDeclare; class AddressSchema extends SchemaDeclare { function schema()

    { $this->column('address') ->timestamp(); } } 12年4月22⽇日星期⽇日
  31. use LazyRecord\Schema\SchemaDeclare; class NameSchema extends SchemaDeclare { function schema() {

    $this->column('created_on') ->date() ->isa('DateTime') ->deflator( function($val) { if( is_a( $val, 'DateTime' ) ) return $val->format('Y-m-d'); elseif( is_integer($val) ) { return strftime( '%Y-%m-%d' , $val ); } return $val; }) ->inflator( function($val) { return new \DateTime( $val ); }); } } 12年4月22⽇日星期⽇日
  32. data_sources: master: dsn: 'mysql:host=localhost;dbname=lazy_test' user: root pass: 123123 slave: dsn:

    'mysql:dbname=lazy_test' query_options: { quote_column: true, quote_table: true } database.yml 12年4月22⽇日星期⽇日
  33. // data source for writing $this->writeTo('master'); // data source for

    reading $this->readFrom('slave'); data_sources: master: dsn: 'mysql:host=localhost;dbname=lazy_test' user: root pass: 123123 slave: dsn: 'mysql:dbname=lazy_test' query_options: { quote_column: true, quote_table: true } database.yml schema 12年4月22⽇日星期⽇日
  34. --- bootstrap: - bootstrap.php schema: paths: - model data_sources: default:

    dsn: 'sqlite:/tmp/todos.db' config/database.yml 12年4月22⽇日星期⽇日
  35. $ lazy build-conf config/database.yml Convert YAML to PHP. <?php $config

    = require '.lazy.php'; APC caches this automatically. 12年4月22⽇日星期⽇日
  36. <?php class TodoSchema extends LazyRecord\Schema\SchemaDeclare { function schema() { $this->column('id')

    ->primary() ->autoIncrement() ->integer(); $this->column('title') ->text(); $this->column('done') ->boolean() ->default(false); $this->column('created_on') ->defaultBuilder( function() { return date('c'); } ) ->timestamp(); } function bootstrap($model) { $model->create(array( 'title' => 'Foo', )); } } 12年4月22⽇日星期⽇日
  37. $ lazy build-schema model/TodoSchema.php ... Classmap: ! TodoSchemaProxy => model/TodoSchemaProxy.php

    ! TodoBase => model/TodoBase.php ! Todo => model/Todo.php ! TodoCollectionBase => model/TodoCollectionBase.php ! TodoCollection => model/TodoCollection.php Done 12年4月22⽇日星期⽇日
  38. $ lazy build-sql model/TodoSchema.php Building SQL for TodoSchema --- SQL

    for TodoSchema CREATE TABLE todos ( id integer primary key autoincrement, title text, done boolean default 0, created_on timestamp ); 12年4月22⽇日星期⽇日
  39. <?php use LazyRecord\ConfigLoader; $config = new ConfigLoader; $config->load( __DIR__ .

    '/.lazy.php'); $config->init(); 12年4月22⽇日星期⽇日
  40. Roller Router • APC cache • FileSystem cache • Use

    Array to store routes • through PHP extension, can dispatch 1607% faster than pure php version • Annotation reader support • RESTful plugin 12年4月22⽇日星期⽇日
  41. $router = new Roller\Router; $router->get( '/blog/:id/:title' , function($id,$title) { return

    'Blog'; }); $router->post( '/blog/:year/:month/:id/:title', array('Controller','method') ); $router->any( '/path/to/:year' , array('Callback','method') , array( 'year' => '\d+', )); 12年4月22⽇日星期⽇日
  42. <?php $subroutes = new Roller\RouteSet; $subroutes->add( '/subitem' , $cb );

    $routes = new Roller\RouteSet; $routes->mount( '/item' , $subroutes ); /item/subitem => $cb RouteSet 12年4月22⽇日星期⽇日
  43. Dispatch $r = $router->dispatch( isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '/' );

    if( $r ) { echo $r(); } else { die('Page not found'); } 12年4月22⽇日星期⽇日
  44. <?php $router = new Roller\Router( null, array( 'cache_id' => 'router_demo'

    )); $restful = new Roller\Plugin\RESTful(array( 'prefix' => '/=/restful' )); $restful->setGenericHandler( 'MyGenericHandler' ); $router->addPlugin($restful); 12年4月22⽇日星期⽇日
  45. GET /=/restful/posts GET /=/restful/posts.json GET /=/restful/posts.yml GET /=/restful/posts/23 GET /=/restful/posts/23.json

    POST /=/restful/posts/23 DELETE /=/restful/posts/23 Auto-generated routes 12年4月22⽇日星期⽇日
  46. <?php use Roller\Plugin\RESTful\ResourceHandler; use Roller\Plugin\RESTful\GenericHandler; class MyGenericHandler extends GenericHandler {

    public function create($resource) { } public function load($resource,$id) { } public function update($resource,$id) { } public function delete($resource,$id) { } public function find($resource) { } } 12年4月22⽇日星期⽇日
  47. <?php namespace LazyBone\Resource; use Roller\Plugin\RESTful\ResourceHandler; use Todo; use TodoCollection; class

    TodoResource extends ResourceHandler { public function create() { $vars = json_decode($this->readInput(),true); $todo = new Todo; $ret = $todo->create($vars); if( $ret->success ) { return $todo->toArray(); } $this->codeBadRequest(); return array( 'error' => $ret->message ); } public function update($id) { $todo = new Todo( $id ); if( ! $todo->id ) { return $this->codeNotFound(); } $vars = json_decode($this->readInput(),true); unset( $vars['created_on'] ); // lazy record bug if($vars) { $todo->update( $vars ); return $todo->toArray(); } return $this->codeBadRequest(); } .... 12年4月22⽇日星期⽇日
  48. Todo = Backbone.Model.extend({ // Default attributes for the todo item.

    defaults: function() { return { title: "empty todo...", done: false // order: Todos.nextOrder(), }; }, // Toggle the `done` state of this todo item. toggle: function() { this.save({done: !this.get("done")}); }, clear: function() { this.destroy(); } }); 12年4月22⽇日星期⽇日
  49. TodoList = Backbone.Collection.extend({ // Reference to this collection's model. model:

    Todo, url:"/=/todos", done: function() { return this.filter(function(todo){ return todo.get('done'); }); }, remaining: function() { return this.without.apply(this, this.done()); }, }); 12年4月22⽇日星期⽇日