Improved model performance a little bit

This commit is contained in:
Marcin Kurczewski 2014-05-09 17:54:52 +02:00
parent 8ee80ea170
commit 343268d029
16 changed files with 196 additions and 163 deletions

View file

@ -9,7 +9,22 @@ abstract class AbstractCrudModel implements IModel
public static function spawn() public static function spawn()
{ {
$entityClassName = static::getEntityClassName(); $entityClassName = static::getEntityClassName();
return new $entityClassName(); $entity = new $entityClassName(new static);
$entity->fillNew();
return $entity;
}
public static function spawnFromDatabaseRows($input)
{
return array_map([get_called_class(), 'spawnFromDatabaseRow'], $input);
}
public static function spawnFromDatabaseRow($row)
{
$entityClassName = static::getEntityClassName();
$entity = new $entityClassName(new static);
$entity->fillFromDatabase($row);
return $entity;
} }
public static function remove($entities) public static function remove($entities)
@ -40,7 +55,7 @@ abstract class AbstractCrudModel implements IModel
$row = Database::fetchOne($stmt); $row = Database::fetchOne($stmt);
return $row return $row
? static::convertRow($row) ? static::spawnFromDatabaseRow($row)
: null; : null;
} }
@ -53,7 +68,7 @@ abstract class AbstractCrudModel implements IModel
$rows = Database::fetchAll($stmt); $rows = Database::fetchAll($stmt);
if ($rows) if ($rows)
return static::convertRows($rows); return static::spawnFromDatabaseRows($rows);
return []; return [];
} }
@ -76,55 +91,6 @@ abstract class AbstractCrudModel implements IModel
return $entityClassName; return $entityClassName;
} }
public static function convertRow($row)
{
$entity = static::spawn();
//todo: force this to be implemented by children
//instead of providing clumsy generic solution
if (isset($row['id']))
$row['id'] = (int) $row['id'];
foreach ($row as $key => $val)
{
if (isset(self::$keyCache[$key]))
{
$key = self::$keyCache[$key];
}
else
{
$key = self::$keyCache[$key] = TextCaseConverter::convert($key,
TextCaseConverter::SNAKE_CASE,
TextCaseConverter::LOWER_CAMEL_CASE);
}
if (property_exists($entity, $key))
{
$reflectionProperty = new ReflectionProperty(get_class($entity), $key);
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($entity, $val);
}
else
{
$entity->$key = $val;
}
}
return $entity;
}
public static function convertRows(array $rows)
{
$entities = [];
foreach ($rows as $i => $row)
{
$entities[$i] = static::convertRow($row);
}
return $entities;
}
public static function forgeId($entity) public static function forgeId($entity)
{ {
$table = static::getTableName(); $table = static::getTableName();

View file

@ -9,16 +9,10 @@ final class CommentModel extends AbstractCrudModel
return 'comment'; return 'comment';
} }
public static function spawn()
{
$comment = new CommentEntity;
$comment->setCreationTime(time());
return $comment;
}
public static function save($comment) public static function save($comment)
{ {
$comment->validate(); $comment->validate();
$comment->getPost()->removeCache('comment_count');
Database::transaction(function() use ($comment) Database::transaction(function() use ($comment)
{ {
@ -47,6 +41,7 @@ final class CommentModel extends AbstractCrudModel
{ {
Database::transaction(function() use ($comment) Database::transaction(function() use ($comment)
{ {
$comment->getPost()->removeCache('comment_count');
$stmt = new Sql\DeleteStatement(); $stmt = new Sql\DeleteStatement();
$stmt->setTable('comment'); $stmt->setTable('comment');
$stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($comment->getId()))); $stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($comment->getId())));
@ -65,7 +60,7 @@ final class CommentModel extends AbstractCrudModel
$rows = Database::fetchAll($stmt); $rows = Database::fetchAll($stmt);
if ($rows) if ($rows)
return self::convertRows($rows); return self::spawnFromDatabaseRows($rows);
return []; return [];
} }

View file

@ -1,9 +1,18 @@
<?php <?php
abstract class AbstractEntity implements IValidatable abstract class AbstractEntity implements IValidatable
{ {
protected $model;
protected $id; protected $id;
protected $__cache = []; protected $__cache = [];
public abstract function fillNew();
public abstract function fillFromDatabase($row);
public function __construct($model)
{
$this->model = $model;
}
public function getId() public function getId()
{ {
return $this->id; return $this->id;
@ -14,16 +23,6 @@ abstract class AbstractEntity implements IValidatable
$this->id = $id; $this->id = $id;
} }
public function resetCache()
{
$this->__cache = [];
}
public function setCache($key, $value)
{
$this->__cache[$key] = $value;
}
public function getCache($key) public function getCache($key)
{ {
return isset($this->__cache[$key]) return isset($this->__cache[$key])
@ -31,8 +30,37 @@ abstract class AbstractEntity implements IValidatable
: null; : null;
} }
public function setCache($key, $value)
{
$this->__cache[$key] = $value;
}
public function removeCache($key)
{
unset($this->__cache[$key]);
}
public function resetCache()
{
$this->__cache = [];
}
public function hasCache($key) public function hasCache($key)
{ {
return isset($this->__cache[$key]); return isset($this->__cache[$key]);
} }
protected function getColumnWithCache($columnName)
{
if ($this->hasCache($columnName))
return $this->getCache($columnName);
$stmt = new \Chibi\Sql\SelectStatement();
$stmt->setTable($this->model->getTableName());
$stmt->setColumn($columnName);
$stmt->setCriterion(new \Chibi\Sql\EqualsFunctor('id', new \Chibi\Sql\Binding($this->getId())));
$value = \Chibi\Database::fetchOne($stmt)[$columnName];
$this->setCache($columnName, $value);
return $value;
}
} }

View file

@ -1,10 +1,24 @@
<?php <?php
final class CommentEntity extends AbstractEntity implements IValidatable final class CommentEntity extends AbstractEntity implements IValidatable
{ {
protected $text; private $text;
protected $postId; private $postId;
protected $commentDate; private $commentDate;
protected $commenterId; private $commenterId;
public function fillNew()
{
$this->commentDate = time();
}
public function fillFromDatabase($row)
{
$this->id = (int) $row['id'];
$this->text = $row['text'];
$this->postId = (int) $row['post_id'];
$this->commentDate = $row['comment_date'];
$this->commenterId = (int) $row['commenter_id'];
}
public function validate() public function validate()
{ {

View file

@ -2,7 +2,7 @@
use \Chibi\Sql as Sql; use \Chibi\Sql as Sql;
use \Chibi\Database as Database; use \Chibi\Database as Database;
class PostEntity extends AbstractEntity implements IValidatable final class PostEntity extends AbstractEntity implements IValidatable
{ {
protected $type; protected $type;
protected $name; protected $name;
@ -18,9 +18,38 @@ class PostEntity extends AbstractEntity implements IValidatable
protected $uploaderId; protected $uploaderId;
protected $source; protected $source;
protected $commentCount = 0; public function fillNew()
protected $favCount = 0; {
protected $score = 0; $this->setSafety(new PostSafety(PostSafety::Safe));
$this->setHidden(false);
$this->setCreationTime(time());
do
{
$this->setName(md5(mt_rand() . uniqid()));
}
while (file_exists($this->getFullPath()));
}
public function fillFromDatabase($row)
{
$this->id = (int) $row['id'];
$this->name = $row['name'];
$this->origName = $row['orig_name'];
$this->fileHash = $row['file_hash'];
$this->fileSize = (int) $row['file_size'];
$this->mimeType = $row['mime_type'];
$this->hidden = (bool) $row['hidden'];
$this->uploadDate = $row['upload_date'];
$this->imageWidth = (int) $row['image_width'];
$this->imageHeight = (int) $row['image_height'];
$this->uploaderId = (int) $row['uploader_id'];
$this->source = $row['source'];
$this->setCache('comment_count', $row['comment_count']);
$this->setCache('fav_count', $row['fav_count']);
$this->setCache('score', $row['score']);
$this->setType(new PostType($row['type']));
$this->setSafety(new PostSafety($row['safety']));
}
public function validate() public function validate()
{ {
@ -80,24 +109,24 @@ class PostEntity extends AbstractEntity implements IValidatable
$stmt->addInnerJoin('favoritee', new Sql\EqualsFunctor('favoritee.user_id', 'user.id')); $stmt->addInnerJoin('favoritee', new Sql\EqualsFunctor('favoritee.user_id', 'user.id'));
$stmt->setCriterion(new Sql\EqualsFunctor('favoritee.post_id', new Sql\Binding($this->getId()))); $stmt->setCriterion(new Sql\EqualsFunctor('favoritee.post_id', new Sql\Binding($this->getId())));
$rows = Database::fetchAll($stmt); $rows = Database::fetchAll($stmt);
$favorites = UserModel::convertRows($rows); $favorites = UserModel::spawnFromDatabaseRows($rows);
$this->setCache('favoritee', $favorites); $this->setCache('favoritee', $favorites);
return $favorites; return $favorites;
} }
public function getScore() public function getScore()
{ {
return $this->score; return (int) $this->getColumnWithCache('score');
} }
public function getCommentCount() public function getCommentCount()
{ {
return $this->commentCount; return (int) $this->getColumnWithCache('comment_count');
} }
public function getFavoriteCount() public function getFavoriteCount()
{ {
return $this->favCount; return (int) $this->getColumnWithCache('fav_count');
} }
public function getRelations() public function getRelations()
@ -119,7 +148,7 @@ class PostEntity extends AbstractEntity implements IValidatable
->add(new Sql\EqualsFunctor('post.id', 'crossref.post_id')) ->add(new Sql\EqualsFunctor('post.id', 'crossref.post_id'))
->add(new Sql\EqualsFunctor('crossref.post2_id', $binding)))); ->add(new Sql\EqualsFunctor('crossref.post2_id', $binding))));
$rows = Database::fetchAll($stmt); $rows = Database::fetchAll($stmt);
$posts = PostModel::convertRows($rows); $posts = PostModel::spawnFromDatabaseRows($rows);
$this->setCache('relations', $posts); $this->setCache('relations', $posts);
return $posts; return $posts;
} }

View file

@ -2,9 +2,22 @@
use \Chibi\Sql as Sql; use \Chibi\Sql as Sql;
use \Chibi\Database as Database; use \Chibi\Database as Database;
class TagEntity extends AbstractEntity implements IValidatable final class TagEntity extends AbstractEntity implements IValidatable
{ {
protected $name; private $name;
public function fillNew()
{
}
public function fillFromDatabase($row)
{
$this->id = (int) $row['id'];
$this->name = $row['name'];
if (isset($row['post_count']))
$this->setCache('post_count', (int) $row['post_count']);
}
public function validate() public function validate()
{ {

View file

@ -1,10 +1,23 @@
<?php <?php
class TokenEntity extends AbstractEntity implements IValidatable final class TokenEntity extends AbstractEntity implements IValidatable
{ {
protected $userId; private $userId;
protected $token; private $token;
protected $used; private $used;
protected $expires; private $expires;
public function fillNew()
{
}
public function fillFromDatabase($row)
{
$this->id = (int) $row['id'];
$this->userId = (int) $row['user_id'];
$this->token = $row['token'];
$this->used = (bool) $row['used'];
$this->expires = $row['expires'];
}
public function validate() public function validate()
{ {

View file

@ -2,7 +2,7 @@
use \Chibi\Sql as Sql; use \Chibi\Sql as Sql;
use \Chibi\Database as Database; use \Chibi\Database as Database;
class UserEntity extends AbstractEntity implements IValidatable final class UserEntity extends AbstractEntity implements IValidatable
{ {
protected $name; protected $name;
protected $passSalt; protected $passSalt;
@ -19,6 +19,28 @@ class UserEntity extends AbstractEntity implements IValidatable
protected $__passwordChanged = false; protected $__passwordChanged = false;
protected $__password; protected $__password;
public function fillNew()
{
$this->setAccessRank(new AccessRank(AccessRank::Anonymous));
$this->setPasswordSalt(md5(mt_rand() . uniqid()));
}
public function fillFromDatabase($row)
{
$this->id = (int) $row['id'];
$this->name = $row['name'];
$this->passSalt = $row['pass_salt'];
$this->passHash = $row['pass_hash'];
$this->staffConfirmed = $row['staff_confirmed'];
$this->emailUnconfirmed = $row['email_unconfirmed'];
$this->emailConfirmed = $row['email_confirmed'];
$this->joinDate = $row['join_date'];
$this->lastLoginDate = $row['last_login_date'];
$this->settings = $row['settings'];
$this->banned = $row['banned'];
$this->setAccessRank(new AccessRank($row['access_rank']));
}
public function validate() public function validate()
{ {
$this->validateUserName(); $this->validateUserName();

View file

@ -2,40 +2,13 @@
use \Chibi\Sql as Sql; use \Chibi\Sql as Sql;
use \Chibi\Database as Database; use \Chibi\Database as Database;
class PostModel extends AbstractCrudModel final class PostModel extends AbstractCrudModel
{ {
public static function getTableName() public static function getTableName()
{ {
return 'post'; return 'post';
} }
public static function convertRow($row)
{
$entity = parent::convertRow($row);
if (isset($row['type']))
$entity->setType(new PostType($row['type']));
if (isset($row['safety']))
$entity->setSafety(new PostSafety($row['safety']));
return $entity;
}
public static function spawn()
{
$post = new PostEntity;
$post->setSafety(new PostSafety(PostSafety::Safe));
$post->setHidden(false);
$post->setCreationTime(time());
do
{
$post->setName(md5(mt_rand() . uniqid()));
}
while (file_exists($post->getFullPath()));
return $post;
}
public static function save($post) public static function save($post)
{ {
$post->validate(); $post->validate();
@ -162,7 +135,7 @@ class PostModel extends AbstractCrudModel
$row = Database::fetchOne($stmt); $row = Database::fetchOne($stmt);
return $row return $row
? self::convertRow($row) ? self::spawnFromDatabaseRow($row)
: null; : null;
} }
@ -192,7 +165,7 @@ class PostModel extends AbstractCrudModel
$row = Database::fetchOne($stmt); $row = Database::fetchOne($stmt);
return $row return $row
? self::convertRow($row) ? self::spawnFromDatabaseRow($row)
: null; : null;
} }
@ -224,8 +197,7 @@ class PostModel extends AbstractCrudModel
{ {
if (isset($comments[$row['id']])) if (isset($comments[$row['id']]))
continue; continue;
unset($row['post_id']); $comment = CommentModel::spawnFromDatabaseRow($row);
$comment = CommentModel::convertRow($row);
$comments[$row['id']] = $comment; $comments[$row['id']] = $comment;
} }
@ -267,7 +239,7 @@ class PostModel extends AbstractCrudModel
if (isset($tags[$row['id']])) if (isset($tags[$row['id']]))
continue; continue;
unset($row['post_id']); unset($row['post_id']);
$tag = TagModel::convertRow($row); $tag = TagModel::spawnFromDatabaseRow($row);
$tags[$row['id']] = $tag; $tags[$row['id']] = $tag;
} }

View file

@ -2,7 +2,7 @@
use \Chibi\Sql as Sql; use \Chibi\Sql as Sql;
use \Chibi\Database as Database; use \Chibi\Database as Database;
class PropertyModel implements IModel final class PropertyModel implements IModel
{ {
const FeaturedPostId = 0; const FeaturedPostId = 0;
const FeaturedPostUserName = 1; const FeaturedPostUserName = 1;

View file

@ -56,7 +56,7 @@ abstract class AbstractSearchService
{ {
$modelClassName = self::getModelClassName(); $modelClassName = self::getModelClassName();
$rows = static::getEntitiesRows($searchQuery, $perPage, $page); $rows = static::getEntitiesRows($searchQuery, $perPage, $page);
return $modelClassName::convertRows($rows); return $modelClassName::spawnFromDatabaseRows($rows);
} }
public static function getEntityCount($searchQuery) public static function getEntityCount($searchQuery)

View file

@ -62,7 +62,7 @@ class TagSearchService extends AbstractSearchService
usort($rows, function($a, $b) { return intval($b['sort']) - intval($a['sort']); }); usort($rows, function($a, $b) { return intval($b['sort']) - intval($a['sort']); });
return TagModel::convertRows($rows); return TagModel::spawnFromDatabaseRows($rows);
} }
public static function getMostUsedTag() public static function getMostUsedTag()
@ -74,6 +74,6 @@ class TagSearchService extends AbstractSearchService
->setGroupBy('post_tag.tag_id') ->setGroupBy('post_tag.tag_id')
->setOrderBy('post_count', Sql\SelectStatement::ORDER_DESC) ->setOrderBy('post_count', Sql\SelectStatement::ORDER_DESC)
->setLimit(1, 0); ->setLimit(1, 0);
return TagModel::convertRow(Database::fetchOne($stmt)); return TagModel::spawnFromDatabaseRow(Database::fetchOne($stmt));
} }
} }

View file

@ -2,21 +2,13 @@
use \Chibi\Sql as Sql; use \Chibi\Sql as Sql;
use \Chibi\Database as Database; use \Chibi\Database as Database;
class TagModel extends AbstractCrudModel final class TagModel extends AbstractCrudModel
{ {
public static function getTableName() public static function getTableName()
{ {
return 'tag'; return 'tag';
} }
public static function convertRow($row)
{
$entity = parent::convertRow($row);
if (isset($row['post_count']))
$entity->setCache('post_count', $row['post_count']);
return $entity;
}
public static function save($tag) public static function save($tag)
{ {
$tag->validate(); $tag->validate();
@ -126,7 +118,7 @@ class TagModel extends AbstractCrudModel
$rows = Database::fetchAll($stmt); $rows = Database::fetchAll($stmt);
if ($rows) if ($rows)
return self::convertRows($rows); return self::spawnFromDatabaseRows($rows);
return []; return [];
} }
@ -147,7 +139,7 @@ class TagModel extends AbstractCrudModel
$row = Database::fetchOne($stmt); $row = Database::fetchOne($stmt);
return $row return $row
? self::convertRow($row) ? self::spawnFromDatabaseRow($row)
: null; : null;
} }

View file

@ -2,7 +2,7 @@
use \Chibi\Sql as Sql; use \Chibi\Sql as Sql;
use \Chibi\Database as Database; use \Chibi\Database as Database;
class TokenModel extends AbstractCrudModel final class TokenModel extends AbstractCrudModel
{ {
public static function getTableName() public static function getTableName()
{ {
@ -57,7 +57,7 @@ class TokenModel extends AbstractCrudModel
$row = Database::fetchOne($stmt); $row = Database::fetchOne($stmt);
return $row return $row
? self::convertRow($row) ? self::spawnFromDatabaseRow($row)
: null; : null;
} }

View file

@ -2,7 +2,7 @@
use \Chibi\Sql as Sql; use \Chibi\Sql as Sql;
use \Chibi\Database as Database; use \Chibi\Database as Database;
class UserModel extends AbstractCrudModel final class UserModel extends AbstractCrudModel
{ {
const SETTING_SAFETY = 1; const SETTING_SAFETY = 1;
const SETTING_ENDLESS_SCROLLING = 2; const SETTING_ENDLESS_SCROLLING = 2;
@ -14,24 +14,6 @@ class UserModel extends AbstractCrudModel
return 'user'; return 'user';
} }
public static function convertRow($row)
{
$entity = parent::convertRow($row);
if (isset($row['access_rank']))
$entity->setAccessRank(new AccessRank($row['access_rank']));
return $entity;
}
public static function spawn()
{
$user = new UserEntity();
$user->setAccessRank(new AccessRank(AccessRank::Anonymous));
$user->setPasswordSalt(md5(mt_rand() . uniqid()));
return $user;
}
public static function save($user) public static function save($user)
{ {
$user->validate(); $user->validate();
@ -118,7 +100,7 @@ class UserModel extends AbstractCrudModel
$row = Database::fetchOne($stmt); $row = Database::fetchOne($stmt);
return $row return $row
? self::convertRow($row) ? self::spawnFromDatabaseRow($row)
: null; : null;
} }
@ -144,7 +126,7 @@ class UserModel extends AbstractCrudModel
$row = Database::fetchOne($stmt); $row = Database::fetchOne($stmt);
return $row return $row
? self::convertRow($row) ? self::spawnFromDatabaseRow($row)
: null; : null;
} }
@ -154,6 +136,7 @@ class UserModel extends AbstractCrudModel
{ {
Database::transaction(function() use ($user, $post, $score) Database::transaction(function() use ($user, $post, $score)
{ {
$post->removeCache('score');
$stmt = new Sql\DeleteStatement(); $stmt = new Sql\DeleteStatement();
$stmt->setTable('post_score'); $stmt->setTable('post_score');
$stmt->setCriterion((new Sql\ConjunctionFunctor) $stmt->setCriterion((new Sql\ConjunctionFunctor)
@ -177,6 +160,7 @@ class UserModel extends AbstractCrudModel
{ {
Database::transaction(function() use ($user, $post) Database::transaction(function() use ($user, $post)
{ {
$post->removeCache('fav_count');
self::removeFromUserFavorites($user, $post); self::removeFromUserFavorites($user, $post);
$stmt = new Sql\InsertStatement(); $stmt = new Sql\InsertStatement();
$stmt->setTable('favoritee'); $stmt->setTable('favoritee');
@ -191,6 +175,7 @@ class UserModel extends AbstractCrudModel
{ {
Database::transaction(function() use ($user, $post) Database::transaction(function() use ($user, $post)
{ {
$post->removeCache('fav_count');
$stmt = new Sql\DeleteStatement(); $stmt = new Sql\DeleteStatement();
$stmt->setTable('favoritee'); $stmt->setTable('favoritee');
$stmt->setCriterion((new Sql\ConjunctionFunctor) $stmt->setCriterion((new Sql\ConjunctionFunctor)

View file

@ -18,12 +18,16 @@ class ListCommentJobTest extends AbstractTest
$this->assert->areEqual(0, CommentModel::getCount()); $this->assert->areEqual(0, CommentModel::getCount());
$this->mockComment($this->mockUser()); $comment = $this->mockComment($this->mockUser());
$ret = $this->runApi(1); $ret = $this->runApi(1);
$this->assert->areEqual(1, count($ret->entities)); $this->assert->areEqual(1, count($ret->entities));
$post = $ret->entities[0]; $post = $ret->entities[0];
$newComment = $post->getComments()[0];
$this->assert->areEqual($comment->getPostId(), $newComment->getPostId());
$this->assert->areEqual($comment->getPost()->getId(), $newComment->getPost()->getId());
$samePost = $this->assert->doesNotThrow(function() use ($post) $samePost = $this->assert->doesNotThrow(function() use ($post)
{ {
return PostModel::getById($post->getId()); return PostModel::getById($post->getId());