From fe1751cf9e7e4185b8b89c6dbdd38bcd24f82470 Mon Sep 17 00:00:00 2001 From: tpetry Date: Tue, 25 Feb 2025 11:30:51 +0100 Subject: [PATCH] feat: laravel quickstart --- quick-start/laravel.md | 458 ++++++++++++++++++++++++++++++++++++++++ quick-start/laravel.png | Bin 0 -> 8101 bytes 2 files changed, 458 insertions(+) create mode 100644 quick-start/laravel.md create mode 100644 quick-start/laravel.png diff --git a/quick-start/laravel.md b/quick-start/laravel.md new file mode 100644 index 0000000000..b668bb1c94 --- /dev/null +++ b/quick-start/laravel.md @@ -0,0 +1,458 @@ +--- +title: "Quick Start: Laravel and Timescale" +excerpt: Get started with Timescale Cloud or TimescaleDB using Laravel +keywords: [Laravel] +--- + +import QuickstartIntro from "versionContent/_partials/_quickstart-intro.mdx"; + +# Laravel quick start + + + +This quick start guide shows you how to: + +* [Connect to a Timescale service][connect] +* [Create a relational table][create-table] +* [Create a hypertable][create-a-hypertable] +* [Insert data][insert] +* [Execute a query][query] +* [Explore aggregation functions][explore] + +## Prerequisites + +Before you start, make sure you have: + +* Created a Timescale service. For more information, see the + [start up documentation][install]. Make a note of the Connection Information + in the Timescale service that you created. +* Installed the [Laravel][laravel-installation] Installer. + +## Connect to a Timescale service + +In this section, you create a connection to your Timescale service through the Laravel +application. + + + + + +1. Create a new Laravel application configured to use PostgreSQL as the database. + Your Timescale service works as a PostgreSQL extension. + Don't run migrations yet, as the connection information will be provided in the next step. + + ```bash + laravel new timescale-app + ``` + + Now switch directory to the newly created Laravel application and install the JavaScript dependencies: + + ```bash + cd timescale-app + ``` + +1. Update the database credentials in the `.env` file of your Laravel application with the + information shown when the Timescale service was created. + + ```ini + DB_CONNECTION=pgsql + DB_HOST= + DB_PORT= + DB_DATABASE= + DB_USERNAME= + DB_PASSWORD= + ``` + +1. Install the [Enhanced PostgreSQL driver for Laravel](https://github.com/tpetry/laravel-postgresql-enhanced): + + ```ruby + composer require tpetry/laravel-postgresql-enhanced + ``` + +1. Create a new Laravel migration: + + ```ruby + php artisan make:migration activate_timescale + ``` + + A new migration file `_activate_timescale.php` is created in + the `database/migrations` directory. Activate the Timescale extension: + + ```php + + + + +## Create a relational table + +In this section, you create a table to store the user agent or browser and +Laravel execution time for every visitor loading a page. You could easily +extend this simple example to store a host of additional analytics information +interesting to you. + + + + + +1. Generate a model and migration for the table: + + ```ruby + php artisan make:model --migration PageLoad + ``` + +1. Change the migration code in the `_create_page_loads_table.php` + file located at the `database/migrations` directory to: + + ```php + identity(always: true); + $table->ipAddress('ip'); + $table->text('url'); + $table->text('user_agent'); + $table->float('runtime'); + $table->timestampsTz(); + + $table->primary(['id', 'created_at']); + }); + } + }; + ``` + + + In the next chapter, you will instruct Timescale to automatically partition the table and + create new hypertable chunks by the `created_at` column. However, Timescale requires that + any `UNIQUE` or `PRIMARY KEY` indexes on the table include all partitioning columns, which + in this case is the `created_at` column. + + Primary keys with multiple columns are not supported by Eloquent so you can't use the automatic + route model binding of Laravel. For acceptable performance on big tables, you should load the rows + by all primary key columns. + + +1. Run the migration to create the table: + + ```ruby + php artisan migrate + ``` + +1. Change the generated model in `app/Models/PageLoad.php` to allow filling the migration columns: + + ```php + + + + +## Create a hypertable + +When you have created the relational table, you can transform it to a hypertable. +All operation like adding indexes, altering columns, inserting data, selecting data, +and most other tasks are executed like you always do in Laravel. A hypertable is just +a regular table with more superpowers. + + + + + +1. Create a migration to modify the `page_loads` table and transform it to a hypertable: + + ```ruby + php artisan make:migration --table=page_loads make_hypertable + ``` + + A new migration file `_make_hypertable.php` is created in + the `database/migrations` directory. + +1. Change the migration code in the `_make_hypertable.php` + file located at the `database/migrations` directory to: + + ```php + timescale(new CreateHypertable('created_at', interval: '14 days')); + }); + } + }; + ``` + +1. Run the migration: + + ```ruby + php artisan migrate + ``` + + + + + +## Insert data + +You will now write a simple example to log all requests handled by Laravel to the created +hyptertable within the Timescale service. The `PageLoad` model could in your application +be extended by many more interesting information to build a performance analysis dashboard. + + + + + +1. Create a new middleware to log every request handled by Laravel: + + ```ruby + php artisan make:middleware StorePageLoad + ``` + +1. Extend the generated `StorePageLoad` middleware located at `app/Http/Middleware/StorePageLoad.php` + to save a new `PageLoad` model with the request's information to the database: + + ```php + $request->ip(), + 'url' => $request->fullUrl(), + 'user_agent' => $request->userAgent(), + 'runtime' => microtime(true) - LARAVEL_START, + ]); + + return $response; + } + } + ``` + +1. Register the middleware by appending it to the middleware stack in the `bootstrap/app.php file: + + ```php + ->withMiddleware(function (Middleware $middleware) { + $middleware->append(App\Http\Middleware\StorePageLoad::class); + }) + ``` + +1. Start the Laravel app and open the page multiple times: + + ```bash + php artisan serve + ``` + +1. You can now peek into the database to see the saved `PageLoad` models: + + ```bash + php artisan tinker --execute="dump(App\Models\PageLoad::all()->toArray())" + ``` + + The result will be similar to: + + ```bash + [!] Aliasing 'PageLoad' to 'App\Models\PageLoad' for this Tinker session. + + array:9 [ + 0 => array:7 [ + "id" => 1 + "ip" => "127.0.0.1" + "url" => "http://localhost:8000" + "user_agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" + "runtime" => "0.38390707969666" + "created_at" => "2025-02-25T10:22:43.000000Z" + "updated_at" => "2025-02-25T10:22:43.000000Z" + ] + 1 => array:7 [ + "id" => 2 + "ip" => "127.0.0.1" + "url" => "http://localhost:8000" + "user_agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" + "runtime" => "0.16254186630249" + "created_at" => "2025-02-25T10:22:45.000000Z" + "updated_at" => "2025-02-25T10:22:45.000000Z" + ] + 2 => array:7 [ + "id" => 3 + "ip" => "127.0.0.1" + "url" => "http://localhost:8000" + "user_agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/135.0" + "runtime" => "0.62188597679138" + "created_at" => "2025-02-25T10:23:07.000000Z" + "updated_at" => "2025-02-25T10:23:07.000000Z" + ] + ] + ``` + + + + + +## Explore aggregation functions + +Now that you have performance statics saves, you can explore the data: + + + + + +1. Create an artisan command: + + ```bash + php artisan make:command ViewStatistics + ``` + +1. Modify the created command in `app/Console/Commands/ViewStatistics.php` to + calculate and display daily runtime statistics: + + ```php + select(columns: [ + DB::raw("time_bucket('1 day', created_at) AS day"), + DB::raw('count(*) as count'), + DB::raw('avg(runtime) as runtime_avg'), + DB::raw('approx_percentile(0.50, percentile_agg(runtime)) as runtime_p50'), + DB::raw('approx_percentile(0.95, percentile_agg(runtime)) as runtime_p95'), + DB::raw('array_to_json(histogram(runtime, 0.0, 1.0, 5)) as runtime_histogram'), + ]) + ->withCasts([ + 'runtime_histogram' => 'array', + ]) + ->whereBetween('created_at', [now()->subDays(7)->startOfDay(), now()]) + ->groupBy('day') + ->orderBy('day', 'desc') + ->get() + ->toArray(); + + foreach ($statistics as $statistic) { + $this->newLine(); + + $this->components->twoColumnDetail(''.substr($statistic['day'], 0, 10).'', sprintf('%s requests', number_format($statistic['count']))); + $this->components->twoColumnDetail('Runtime avg', sprintf('%.3fs', $statistic['runtime_avg'])); + $this->components->twoColumnDetail('Runtime p50', sprintf('%.3fs', $statistic['runtime_p50'])); + $this->components->twoColumnDetail('Runtime p95', sprintf('%.3fs', $statistic['runtime_p95'])); + $this->components->twoColumnDetail('Runtime count(0.000s - 0.199s)', number_format($statistic['runtime_histogram'][1])); + $this->components->twoColumnDetail('Runtime count(0.200s - 0.399s)', number_format($statistic['runtime_histogram'][2])); + $this->components->twoColumnDetail('Runtime count(0.400s - 0.599s)', number_format($statistic['runtime_histogram'][3])); + $this->components->twoColumnDetail('Runtime count(0.600s - 0.799s)', number_format($statistic['runtime_histogram'][4])); + $this->components->twoColumnDetail('Runtime count(0.800s - 0.999s)', number_format($statistic['runtime_histogram'][5])); + $this->components->twoColumnDetail('Runtime count(1.000s+)', number_format($statistic['runtime_histogram'][6])); + } + } + } + ``` + +1. Run the command: + + ```bash + php artisan app:view-statistics + ``` + + You should now see statistics like these: + + {insert quick-start/laravel.png image here} + + + + + +## Next steps + +Now that you're able to connect, read, and write to a TimescaleDB instance from +your Laravel application, be sure to check out these advanced TimescaleDB topics: + +* Read up on [Compressions](https://docs.timescale.com/use-timescale/latest/compression/) to make + the stored data up to 90% smaller and [Continuous Aggregates](https://docs.timescale.com/use-timescale/latest/continuous-aggregates/) + to automatically pre-compute your query results for millisecond query times on any data size. +* Refer to the [Enhanced PostgreSQL driver for Laravel](https://github.com/tpetry/laravel-postgresql-enhanced#timescale) documentation + on how to utilize Timescale with Laravel. + +[connect]: #connect-to-timescaledb +[create-table]: #create-a-relational-table +[create-a-hypertable]: #create-a-hypertable +[insert]: #insert-rows-of-data +[query]: #execute-a-query +[explore]: #explore-aggregation-functions +[install]: /getting-started/latest/ +[laravel-installation]: https://laravel.com/docs/11.x/installation +[time_bucket]: /api/:currentVersion:/hyperfunctions/time_bucket/ diff --git a/quick-start/laravel.png b/quick-start/laravel.png new file mode 100644 index 0000000000000000000000000000000000000000..b3d78674837e5b911ba389f86cf5ffbbb5a1dc30 GIT binary patch literal 8101 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 K6l&!x!u|_4WJfLl literal 0 HcmV?d00001