Code Monkey home page Code Monkey logo

mapper's Introduction

Tarantool Mapper

License Testing Latest Version Total Downloads Telegram

Getting started

Supported versions are php 8+ and tarantool 2+.
The recommended way to install the library is through Composer:

composer require tarantool/mapper

Usually, you manage dependencies in your service provider.
To get started you should create client instance and pass it to mapper constructor.
In this example we use PurePacker and StreamConnection.
To see other implementations please check client documentation

use Tarantool\Client\Client;
use Tarantool\Mapper\Mapper;

$client = Client::fromDefaults();
$mapper = new Mapper($client);

// internaly mapper wraps client with special middleware
assert($mapper->client !== $client);

Schema management

To get started you should describe your spaces, their format and indexes.

$person = $mapper->createSpace('person', [
    'engine' => 'memtx',
    'if_not_exists' => true,
]);

// add properties - name, type and options
$person->addProperty('id', 'unsigned');
$person->addProperty('name', 'string');
$person->addProperty('birthday', 'unsigned');
$person->addProperty('gender', 'string', [
    'default' => 'male'
]);

// indexes are created using fields array and optional index configuration
$person->addIndex(['name']);
$person->addIndex(['birthday'], ['unique' => true]);

// index name is fields based, but you can specify any preffered one
$person->addIndex(['name', 'birthday'], [
    'type' => 'hash',
    'name' => 'name_with_birthday',
]);

/**
 * define format using properties
 */
class Tracker
{
    public int $id;
    public int $reference;
    public string $status;

    public static function initSchema(\Tarantool\Mapper\Space $space)
    {
        $space->addIndex(['reference']);
    }
}

$tracker = $mapper->createSpace('tracker');
$tracker->setClass(Tracker::class);
$tracker->migrate();

/**
 * define format using constructor promotion
 */
class Policy
{
    public function __construct(
        public int $id,
        public string $nick,
        public string $status,
    ) {
    }

    public static function initialize(\Tarantool\Mapper\Space $space)
    {
        $space->addIndex(['nick'], ['unique' => true]);
    }
}

$policy = $mapper->createSpace('policy');
$policy->setClass(Policy::class, 'initialize'); // use custom initialize method
$policy->migrate();

Working with the data

Now you can store and retreive data from tarantool storage using mapper instance.

// get space instance
$persons = $mapper->getSpace('person');

// create new entity
$dmitry = $persons->create([
    'id' => 1,
    'name' => 'Dmitry'
]);

// create entities using mapper wrapper.
// this way entity will be created and saved in the tarantool
$vasily = $mapper->create('person', [
    'id' => 2,
    'name' => 'Vasily'
]);

// retreive entites by id using space
$helloWorld = $mapper->getSpace('post')->findOne(['id' => 3]);

// or using mapper wrapper
$helloWorld = $mapper->findOne('post', ['id' => 3]);

// pass client criteria object as well
$criteria = Criteria::index('age')->andKey([18])->andGeIterator();
$adults = $mapper->find('user', $criteria);

// updates are easy
$posts = $mapper->getSpace('post');
$helloWorld = $posts->update($helloWorld, [
    'title' => 'Hello world'
]);

// if you use instance classes, instance would be updated
$policy = $mapper->findOrFail('policy', ['id' => 3]);
$policy = $mapper->get('policy', 3); // getter shortcut
$mapper->update('policy', $policy, [
    'title' => 'updated title',
]);
echo $policy->title; // updated title

// use client operations as well
use Tarantool\Client\Schema\Operations;
$mapper->getSpace('policy')->update($policy, Operations::add('counter', 1));
var_dump($policy->counter); // actual value

Schema Cache

Any new mapper instance will fetch schema from the tarantool, this requests can takes a bit of database load.
Use your favorite psr/cache implementation to persist schema on the application side.
For example, we use apcu adapter from symfony/cache package.
If new schema version is not persisted in cache, mapper will fetch it

use Symfony\Component\Cache\Adapter\ApcuAdapter;
$cache = new ApcuAdapter();

$mapper = new Mapper(Client::fromDefaults());
$mapper->cache = $cache;
$mapper->getSpace('_vspace'); // schema is fetched now

$mapper = new Mapper(Client::fromDefaults());
$mapper->cache = $cache;
$mapper->getSpace('_vspace'); // no new requests are made

Query Cache

If you don't want to repeat select queries you can inject cache interface implementation to space.
Use your favorite psr/cache implementation to persist schema on the application side.
For example, we use array adapter from symfony/cache package.

use Symfony\Component\Cache\Adapter\ArrayAdapter;

$mapper = new Mapper(Client::fromDefaults());
$mapper->getSpace('_vspace')->cache = new ArrayAdapter(); // feel free to set default ttl

$mapper->find('_vspace'); // query is executed
$mapper->find('_vspace'); // results are fetched from cache
$mapper->find('_vspace'); // results are fetched from cache

Changes registration

In some cases you want to get all changes that were made during current session.
By default spy configuration is set to false, this improves performance a bit.

$mapper->spy = true;

$nekufa = $mapper->create('user', ['login' => 'nekufa']);
$firstPost = $mapper->create('post', [
    'user_id' => $nekufa->id,
    'title' => 'hello world',
]);
$mapper->update('post', $firstPost, ['title' => 'Final title']);

// now there are two changes
[$first, $second] = $mapper->getChanges();
echo $first->type; // insert
echo $first->space; // user
echo $first->data; // ['login' => 'nekufa']

// all changes would be merged by space and key
// this reduces changes duplicates
echo $second->type; // insert
echo $second->space; // post
echo $second->data; // ['user_id' => 1, 'title' => 'Final title']

// of course you can flush all changes and start registration from scratch
$mapper->flushChanges();

Multiple connections

If you split your data across multiple tarantool instances you can use prefix based data api.
Api is the same but you prefix space name with a connection prefix.

$pool = new Pool(function (string $prefix) {
    return new Mapper(Client::fromDsn('tcp://' . $prefix));
});

// connect to tarantool instance `volume` and find all timelines.
$trackers = $pool->findOne('volume.timeline');

$nekufa = $pool->findOrCreate('guard.login', ['username' => 'nekufa']);
$pool->update('guard.login', $nekufa, ['locked_at' => time()]);

// pool also wraps changes with the prefixes
echo $pool->getChanges()[0]->space; // guard.login

// all expressions do the same behind the scenes
$pool->find('flow.tracker', ['status' => 'active']);
$pool->getMapper('flow')->find('tracker', ['status' => 'active']);
$pool->getMapper('flow')->getSpace('tracker')->find(['status' => 'active']);

Lua code delivery

Iproto usage is very powerful but sometimes is not enough.
You can easily execute lua code and pass local variables using associative array.

In addition, if you don't want to deliver it every request, use magic call method.
When you use call method, mapper generates unique function name and creates it if it's not exist.

// this method will always deliver and parse lua code on the tarantool side
$mapper->evaluate('return a + b', ['a' => 2, 'b' => 7]); // 9

// first call a function would be created with name evaluate_{BODYHASH}
// there would be two requests - create function and call it
$mapper->call('return a + b', ['a' => 2, 'b' => 7]); // 9

// second call will produce single request with function name and arguments
$mapper->call('return a + b', ['a' => 2, 'b' => 7]); // 9

Migrations

Use basic migration class to implement some logic before or after schema creation.
Pass migrations to mapper migrate method and that's all.


use Tarantool\Mapper\Migration;
use Tarantool\Mapper\Space;

class DropLegacySpaces extends Migration
{
    public function beforeSchema(Mapper $mapper)
    {
        $mapper->call(<<<LUA
            if box.space.legacy then
                box.space.legacy:drop()
                box.space.legacy_detail:drop()
            end
        LUA);
    }
}
class InitializeData extends Migration
{
    public function afterSchema(Mapper $mapper)
}
$mapper = $container->get(Mapper::class);

// also migrate accepts migration instance, or migration class arrays
$mapper->migrate(DropLegacySpaces::class, InitializeData::class);

Performance

We can calculate mapper overhead using getInstance method that is called per each instance.
If you don't use cache, there is single schema fetch on connection and each time schema is upgraded.
Perfomance test was made on (AMD Ryzen 5 3600X), Ubuntu 23.10 using PHP 8.3.6

Instance type Instances per second
constructor 4 664 172
properties 4 328 442
simple array 11 983 040

mapper's People

Contributors

chelseg avatar firetawnyowl avatar lav45 avatar nekufa avatar rybakit avatar vladimir1988 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mapper's Issues

How I can to upsert entity data (from array)

I need to implement upsert method. I can find entity by primary key (it is ok), but how change all entity data (from array) for save entity changes. May be somebody has method upser? (like the method findOrCreate but findAndUpdate).

migrate question

Hi,
I see that migration is possible with this great Tarantool PHP Mapper repo. But there is no function to actually re-run the migration, is it possible for you to add this feature?

Bug with nullable index part

Hello!

I reproduce bug with nullable field who used in index.

My code for creating space:

$this->tarantoolMapper->getSchema()->createSpace('example', [
    'if_not_exists' => true,
    'engine'        => 'memtx',
    'properties' => [
        'id'         => 'unsigned',
        'field1'     => 'string',
        'field2'     => 'string',
        'field3'     => 'unsigned',
        'field4'     => 'unsigned',
        'field5'     => 'unsigned',
        'field6'     => 'unsigned',
    ],
])
->setPropertyNullable('field5')
->addIndex([
    'fields'        => 'id',
    'if_not_exists' => true,
    'sequence'      => true,
    'name'          => 'index_1'
])
->addIndex([
    'fields'        => ['field2', 'field5'],
    'unique'        => false,
    'if_not_exists' => true,
    'name'          => 'index_2',
]);

But, in createIndex function we have some interesting case.

After that space field who define like nullable, stay not nullable.

Extend mapping space

Instead of using independent space, we need to extend mapping space and mark some properties that they are references. This can be achieved by adding new mapping field - property type.
We will put there space or primitive types like integer or string.

SQL table: ->find(['TEST_ID' => 1]) lists all rows but only with the data from first row ?

Hi,
I am using SQL tables with mapper but I have issues with fetching correct data when using SQL tables.

I am using the lastest versions of php-client and php-mapper, the dev repo.

This is how you can re-produce the issue:

tarantoolctl enter myplace
\set language sql
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, some_id INTEGER, test_id INTEGER)
INSERT INTO user (some_id, test_id) VALUES (78, 1)
INSERT INTO user (some_id, test_id) VALUES (43, 2)
INSERT INTO user (some_id, test_id) VALUES (12, 1)

With using php-mapper I am doing this:
$testing = $mapper->getRepository('USERS')->find(['TEST_ID' => 1]);
the result is:

array (size=3)
  0 => 
    object(Tarantool\Mapper\Entity)[602]
      public 'ID' => int 1
      public 'SOME_ID' => int 78
      public 'TEST_ID' => int 1
  1 => 
    object(Tarantool\Mapper\Entity)[602]
      public 'ID' => int 1
      public 'SOME_ID' => int 78
      public 'TEST_ID' => int 2
  2 => 
    object(Tarantool\Mapper\Entity)[602]
      public 'ID' => int 1
      public 'SOME_ID' => int 78
      public 'TEST_ID' => int 1

Which means that is actually just returns all rows but with the content of the first row in the table when it should return:

array (size=3)
  0 => 
    object(Tarantool\Mapper\Entity)[602]
      public 'ID' => int 1
      public 'SOME_ID' => int 78
      public 'TEST_ID' => int 1
  1 => 
    object(Tarantool\Mapper\Entity)[602]
      public 'ID' => int 3
      public 'SOME_ID' => int 12
      public 'TEST_ID' => int 1

If you have any questions regarding this please let me know.
If you can fix this bug I would be very happy.

regards Zilveer

update entities with string primary key

$conn = $this->findOrFail('connection', ['uuid' => $uuid]);
$conn->gateway = $gateway;
$conn->socket = $socket;
$conn->save();

produces insert request, uuid is string

Tarantool v2.3 support -> No space _procedure

Hi,
I have updated to the latest Tarantool v2.3.0-107-ga377aaa9c (I do know that this it is only the development branch but I am using it for testing purpose).

However, when I was using v2.2 I had no problems creating procedures according to the example given at: ProcedureTest.php

But after upgrading to Tarantool v2.3 and purging all data I get the following error message:
No space _procedure

Any kind of help is appreciated.

regards Zilveer

Add repository and instance fabrique

There should be a way to define repository and entity class for each entity.
This way we can add custom methods and logic to entities and repositories.

Bug with sequence plugin

Hello.

I have bug with sequence plugin where sequence is created by create_index with sequence option true.

My code:

$mapper->getSchema()->createSpace('some_space', [
    'is_sync'       => true,
    'if_not_exists' => true,
    'engine'        => 'memtx',
    'properties' => [
        'id'       => 'unsigned',
        'value'    => 'string',
    ],
])
->addIndex([
    'fields'        => 'id',
    'if_not_exists' => true,
    'sequence'      => true,
]);

$mapper->getPlugin(Tarantool\Mapper\Plugin\Sequence::class);
$result = $mapper->create('some_space', ['value'  => $value]);

Exception:

eval:1: variable 'mapper_create_sequence' is not declared {"exception":"[object] (Tarantool\\Client\\Exception\\RequestFailed(code: 32): eval:1: variable 'mapper_create_sequence' is not declared at /application/vendor/tarantool/client/src/Exception/RequestFailed.php:32)
[stacktrace]
#0 /application/vendor/tarantool/client/src/Handler/DefaultHandler.php(48): Tarantool\\Client\\Exception\\RequestFailed::fromErrorResponse(Object(Tarantool\\Client\\Response))
#1 /application/vendor/tarantool/client/src/Handler/MiddlewareHandler.php(63): Tarantool\\Client\\Handler\\DefaultHandler->handle(Object(Tarantool\\Client\\Request\\EvaluateRequest))
#2 /application/vendor/tarantool/client/src/Middleware/AuthenticationMiddleware.php(41): Tarantool\\Client\\Handler\\MiddlewareHandler->handle(Object(Tarantool\\Client\\Request\\EvaluateRequest))
#3 /application/vendor/tarantool/client/src/Handler/MiddlewareHandler.php(69): Tarantool\\Client\\Middleware\\AuthenticationMiddleware->process(Object(Tarantool\\Client\\Request\\EvaluateRequest), Object(Tarantool\\Client\\Handler\\MiddlewareHandler))
#4 /application/vendor/tarantool/client/src/Middleware/RetryMiddleware.php(72): Tarantool\\Client\\Handler\\MiddlewareHandler->handle(Object(Tarantool\\Client\\Request\\EvaluateRequest))
#5 /application/vendor/tarantool/client/src/Handler/MiddlewareHandler.php(69): Tarantool\\Client\\Middleware\\RetryMiddleware->process(Object(Tarantool\\Client\\Request\\EvaluateRequest), Object(Tarantool\\Client\\Handler\\MiddlewareHandler))
#6 /application/vendor/tarantool/client/src/Client.php(170): Tarantool\\Client\\Handler\\MiddlewareHandler->handle(Object(Tarantool\\Client\\Request\\EvaluateRequest))
#7 /application/vendor/tarantool/mapper/src/Plugin/Procedure.php(50): Tarantool\\Client\\Client->evaluate('return _G.mappe...')
#8 /application/vendor/tarantool/mapper/src/Plugin/Procedure.php(43): Tarantool\\Mapper\\Plugin\\Procedure->validatePresence(Object(Tarantool\\Mapper\\Procedure\\CreateSequence))
#9 /application/vendor/tarantool/mapper/src/Plugin/Procedure.php(15): Tarantool\\Mapper\\Plugin\\Procedure->register('Tarantool\\\\Mappe...')
#10 /application/vendor/tarantool/mapper/src/Plugin/Sequence.php(47): Tarantool\\Mapper\\Plugin\\Procedure->get('Tarantool\\\\Mappe...')
#11 /application/vendor/tarantool/mapper/src/Plugin/Sequence.php(58): Tarantool\\Mapper\\Plugin\\Sequence->initializeSequence(Object(Tarantool\\Mapper\\Space))
#12 /application/vendor/tarantool/mapper/src/Plugin/Sequence.php(21): Tarantool\\Mapper\\Plugin\\Sequence->generateValue(Object(Tarantool\\Mapper\\Space))
#13 /application/vendor/tarantool/mapper/src/Repository.php(81): Tarantool\\Mapper\\Plugin\\Sequence->generateKey(Object(Tarantool\\Mapper\\Entity), Object(Tarantool\\Mapper\\Space))
#14 /application/vendor/tarantool/mapper/src/Mapper.php(45): Tarantool\\Mapper\\Repository->create(Array)
#15 /application/app/Components/Category/Repositories/CategoryRepository.php(40): Tarantool\\Mapper\\Mapper->create('some_space', Array)
"}

Tarantool version: 2.6.2
PHP version: 7.4.19

UPD:
This bug happened because sequence name is space_name_seq, but in your code you suppose sequence name is space_name.

Docs issue, $globalSpace->id is: Trying to get property 'id' of non-object

Hi,
first of all thanks for a great plugin!
But it seems to me that the docs isnt "up-to-date".

For example
$globalSpace = $taraMapper->find('_space', ['name' => '_space']); echo $globalSpace->id; // 280
gives me
Trying to get property 'id' of non-object

I dumped the object, and its structure gives me:
array (size=1) 0 => object(Tarantool\Mapper\Entity)[600] public 'id' => int 280

So proper outputting the data in this case it:
$globalSpace[0]->id; which ouputs 280

I assume since Tarantool has been upgraded the Docs needs to be updated here as well.

Once again, thanks for such a great repo!

regards Zilveer

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.