• PG2 release 0.1.4: HoneySQL API and shortcuts

    Table of Content

    PG2 version 0.1.4 is out. In this release, the main feature is improvements made to the pg-honey package which is a wrapper on top of HoneySQL.

    HoneySQL Integration & Shortcuts

    The pg-honey package allows you to call query and execute functions using maps rather than string SQL expressions. Internally, maps are transformed into SQL using the great HoneySQL library. With HoneySQL, you don’t need to format strings to build a SQL, which is clumsy and dangerous in terms of injections.

    The package also provides several shortcuts for such common dutiles as get a single row by id, get a bunch of rows by their ids, insert a row having a map of values, update by a map and so on.

    For a demo, let’s import the package, declare a config map and create a table with some rows as follows:

    (require '[pg.honey :as pgh])
    
    (def config
      {:host "127.0.0.1"
       :port 10140
       :user "test"
       :password "test"
       :dbname "test"})
    
    (def conn
      (pg/connect config))
    
    (pg/query conn "create table test003 (
      id integer not null,
      name text not null,
      active boolean not null default true
    )")
    
    (pg/query conn "insert into test003 (id, name, active)
      values
      (1, 'Ivan', true),
      (2, 'Huan', false),
      (3, 'Juan', true)")
    

    Get by id(s)

    The get-by-id function fetches a single row by a primary key which is :id by default:

    (pgh/get-by-id conn :test003 1)
    ;; {:name "Ivan", :active true, :id 1}
    

    With options, you can specify the name of the primary key and the column names you’re interested in:

    (pgh/get-by-id conn
                   :test003
                   1
                   {:pk [:raw "test003.id"]
                    :fields [:id :name]})
    
    ;; {:name "Ivan", :id 1}
    
    ;; SELECT id, name FROM test003 WHERE test003.id = $1 LIMIT $2
    ;; parameters: $1 = '1', $2 = '1'
    

    The get-by-ids function accepts a collection of primary keys and fetches them using the IN operator. In additon to options that get-by-id has, you can specify the ordering:

    (pgh/get-by-ids conn
                    :test003
                    [1 3 999]
                    {:pk [:raw "test003.id"]
                     :fields [:id :name]
                     :order-by [[:id :desc]]})
    
    [{:name "Juan", :id 3}
     {:name "Ivan", :id 1}]
    
    ;; SELECT id, name FROM test003 WHERE test003.id IN ($1, $2, $3) ORDER BY id DESC
    ;; parameters: $1 = '1', $2 = '3', $3 = '999'
    

    Passing many IDs at once is not recommended. Either pass them by chunks or create a temporary table, COPY IN ids into it and INNER JOIN with the main table.

    Delete

    The delete function removes rows from a table. By default, all the rows are deleted with no filtering, and the deleted rows are returned:

    (pgh/delete conn :test003)
    
    [{:name "Ivan", :active true, :id 1}
     {:name "Huan", :active false, :id 2}
     {:name "Juan", :active true, :id 3}]
    

    You can specify the WHERE clause and the column names of the result:

    (pgh/delete conn
                :test003
                {:where [:and
                         [:= :id 3]
                         [:= :active true]]
                 :returning [:*]})
    
    [{:name "Juan", :active true, :id 3}]
    

    When the :returning option set to nil, no rows are returned.

    Insert (one)

    To observe all the features of the insert function, let’s create a separate table:

    (pg/query conn "create table test004 (
      id serial primary key,
      name text not null,
      active boolean not null default true
    )")
    

    The insert function accepts a collection of maps each represents a row:

    (pgh/insert conn
                :test004
                [{:name "Foo" :active false}
                 {:name "Bar" :active true}]
                {:returning [:id :name]})
    
    [{:name "Foo", :id 1}
     {:name "Bar", :id 2}]
    

    It also accepts options to produce the ON CONFLICT ... DO ... clause known as UPSERT. The following query tries to insert two rows with existing primary keys. Should they exist, the query updates the names of the corresponding rows:

    (pgh/insert conn
                :test004
                [{:id 1 :name "Snip"}
                 {:id 2 :name "Snap"}]
                {:on-conflict [:id]
                 :do-update-set [:name]
                 :returning [:id :name]})
    

    The resulting query looks like this:

    INSERT INTO test004 (id, name) VALUES ($1, $2), ($3, $4)
      ON CONFLICT (id)
      DO UPDATE SET name = EXCLUDED.name
      RETURNING id, name
    parameters: $1 = '1', $2 = 'Snip', $3 = '2', $4 = 'Snap'
    

    The insert-one function acts like insert but accepts and returns a single map. It supports :returning and ON CONFLICT ... clauses as well:

    (pgh/insert-one conn
                    :test004
                    {:id 2 :name "Alter Ego" :active true}
                    {:on-conflict [:id]
                     :do-update-set [:name :active]
                     :returning [:*]})
    
    {:name "Alter Ego", :active true, :id 2}
    

    The logs:

    INSERT INTO test004 (id, name, active) VALUES ($1, $2, TRUE)
      ON CONFLICT (id)
      DO UPDATE SET name = EXCLUDED.name, active = EXCLUDED.active
      RETURNING *
    parameters: $1 = '2', $2 = 'Alter Ego'
    

    Update

    The update function alters rows in a table. By default, it doesn’t do any filtering and returns all the rows affected. The following query sets the boolean active value for all rows:

    (pgh/update conn
                :test003
                {:active true})
    
    [{:name "Ivan", :active true, :id 1}
     {:name "Huan", :active true, :id 2}
     {:name "Juan", :active true, :id 3}]
    

    The :where clause determines conditions for update. You can also specify columns to return:

    (pgh/update conn
                :test003
                {:active false}
                {:where [:= :name "Ivan"]
                 :returning [:id]})
    
    [{:id 1}]
    

    What is great about update is, you can use such complex expressions as increasing counters, negation and so on. Below, we alter the primary key by adding 100 to it, negate the active column, and change the name column with dull concatenation:

    (pgh/update conn
                :test003
                {:id [:+ :id 100]
                 :active [:not :active]
                 :name [:raw "name || name"]}
                {:where [:= :name "Ivan"]
                 :returning [:id :active]})
    
    [{:active true, :id 101}]
    

    Which produces the following query:

    UPDATE test003
      SET
        id = id + $1,
        active = NOT active,
        name = name || name
      WHERE name = $2
      RETURNING id, active
    parameters: $1 = '100', $2 = 'Ivan'
    

    Find (first)

    The find function makes a lookup in a table by column-value pairs. All the pairs are joined using the AND operator:

    (pgh/find conn :test003 {:active true})
    
    [{:name "Ivan", :active true, :id 1}
     {:name "Juan", :active true, :id 3}]
    

    Find by two conditions:

    (pgh/find conn :test003 {:active true
                             :name "Juan"})
    
    [{:name "Juan", :active true, :id 3}]
    
    ;; SELECT * FROM test003 WHERE (active = TRUE) AND (name = $1)
    ;; parameters: $1 = 'Juan'
    

    The function accepts additional options for LIMIT, OFFSET, and ORDER BY clauses:

    (pgh/find conn
              :test003
              {:active true}
              {:fields [:id :name]
               :limit 10
               :offset 1
               :order-by [[:id :desc]]
               :fn-key identity})
    
    [{"id" 1, "name" "Ivan"}]
    
    ;; SELECT id, name FROM test003
    ;;   WHERE (active = TRUE)
    ;;   ORDER BY id DESC
    ;;   LIMIT $1
    ;;   OFFSET $2
    ;; parameters: $1 = '10', $2 = '1'
    

    The find-first function acts the same but returns a single row or nil. Internally, it adds the LIMIT 1 clause to the query:

    (pgh/find-first conn :test003
                    {:active true}
                    {:fields [:id :name]
                     :offset 1
                     :order-by [[:id :desc]]
                     :fn-key identity})
    
    {"id" 1, "name" "Ivan"}
    

    Prepare

    The prepare function makes a prepared statement from a HoneySQL map:

    (def stmt
      (pgh/prepare conn {:select [:*]
                         :from :test003
                         :where [:= :id 0]}))
    
    ;; <Prepared statement, name: s37, param(s): 1, OIDs: [INT8], SQL: SELECT * FROM test003 WHERE id = $1>
    

    Above, the zero value is a placeholder for an integer parameter.

    Now that the statement is prepared, execute it with the right id:

    (pg/execute-statement conn stmt {:params [3]
                                     :first? true})
    
    {:name "Juan", :active true, :id 3}
    

    Alternately, use the [:raw ...] syntax to specify a parameter with a dollar sign:

    (def stmt
      (pgh/prepare conn {:select [:*]
                         :from :test003
                         :where [:raw "id = $1"]}))
    
    (pg/execute-statement conn stmt {:params [1]
                                     :first? true})
    
    {:name "Ivan", :active true, :id 1}
    

    Query and Execute

    There are two general functions called query and execute. Each of them accepts an arbitrary HoneySQL map and performs either Query or Execute request to the server.

    Pay attention that, when using query, a HoneySQL map cannot have parameters. This is a limitation of the Query command. The following query will lead to an error response from the server:

    (pgh/query conn
               {:select [:id]
                :from :test003
                :where [:= :name "Ivan"]
                :order-by [:id]})
    
    ;; Execution error (PGErrorResponse) at org.pg.Accum/maybeThrowError (Accum.java:207).
    ;; Server error response: {severity=ERROR, ... message=there is no parameter $1, verbosity=ERROR}
    

    Instead, use either [:raw ...] syntax or {:inline true} option:

    (pgh/query conn
               {:select [:id]
                :from :test003
                :where [:raw "name = 'Ivan'"] ;; raw (as is)
                :order-by [:id]})
    
    [{:id 1}]
    
    ;; OR
    
    (pgh/query conn
               {:select [:id]
                :from :test003
                :where [:= :name "Ivan"]
                :order-by [:id]}
               {:honey {:inline true}}) ;; inline values
    
    [{:id 1}]
    
    ;; SELECT id FROM test003 WHERE name = 'Ivan' ORDER BY id ASC
    

    The execute function acceps a HoneySQL map with parameters:

    (pgh/execute conn
                   {:select [:id :name]
                    :from :test003
                    :where [:= :name "Ivan"]
                    :order-by [:id]})
    
    [{:name "Ivan", :id 1}]
    

    Both query and execute accept not SELECT only but literally everything: inserting, updating, creating a table, an index, and more. You can build combinations like INSERT ... FROM SELECT or UPDATE ... FROM DELETE to perform complex logic in a single atomic query.

    HoneySQL options

    Any HoneySQL-specific parameter might be passed through the :honey submap in options. Below, we pass the :params map to use the [:param ...] syntax. Also, we produce a pretty-formatted SQL for better logs:

    (pgh/execute conn
                 {:select [:id :name]
                  :from :test003
                  :where [:= :name [:param :name]]
                  :order-by [:id]}
                 {:honey {:pretty true
                          :params {:name "Ivan"}}})
    
    ;; SELECT id, name
    ;; FROM test003
    ;; WHERE name = $1
    ;; ORDER BY id ASC
    ;; parameters: $1 = 'Ivan'
    

    For more options, please refer to the official HoneySQL documentation.

  • PG2 release 0.1.3: Next.JDBC-compatible API

    Table of Content

    PG2 version 0.1.3 is out. One of its new features is a module which mimics Next.JDBC API. Of course, it doesn’t cover 100% of Next.JDBC features yet most of the functions and macros are there. It will help you to introduce PG2 into the project without rewriting all the database-related code from scratch.

    Obtaining a Connection

    In Next.JDBC, all the functions and macros accept something that implements the Connectable protocol. It might be a plain Clojure map, an existing connection, or a connection pool. The PG2 wrapper follows this design. It works with either a map, a connection, or a pool.

    Import the namespace and declare a config:

    (require '[pg.jdbc :as jdbc])
    
    (def config
      {:host "127.0.0.1"
       :port 10140
       :user "test"
       :password "test"
       :dbname "test"})
    

    Having a config map, obtain a connection by passing it into the get-connection function:

    (def conn
      (jdbc/get-connection config))
    

    This approach, although is a part of the Next.JDBC design, is not recommended to use. Once you’ve established a connection, you must either close it or, if it was borrowed from a pool, return it to the pool. There is a special macro on-connection that covers this logic:

    (jdbc/on-connection [bind source]
      ...)
    

    If the source was a map, a new connection is spawned and gets closed afterwards. If the source is a pool, the connection gets returned to the pool. When the source is a connection, nothing happens when exiting the macro.

    (jdbc/on-connection [conn config]
      (println conn))
    

    A brief example with a connection pool and a couple of futures. Each future borrows a connection from a pool, and returns it afterwards.

    (pool/with-pool [pool config]
      (let [f1
            (future
              (jdbc/on-connection [conn1 pool]
                (println
                 (jdbc/execute-one! conn1 ["select 'hoho' as message"]))))
            f2
            (future
              (jdbc/on-connection [conn2 pool]
                (println
                 (jdbc/execute-one! conn2 ["select 'haha' as message"]))))]
        @f1
        @f2))
    
    ;; {:message hoho}
    ;; {:message haha}
    

    Executing Queries

    Two functions execute! and execute-one! send queries to the database. Each of them takes a source, a SQL vector, and a map of options. The SQL vector is a sequence where the first item is either a string or a prepared statement, and the rest values are parameters.

    (jdbc/on-connection [conn config]
      (jdbc/execute! conn ["select $1 as num" 42]))
    ;; [{:num 42}]
    

    Pay attention that parameters use a dollar sign with a number but not a question mark.

    The execute-one! function acts like execute! but returns the first row only. Internaly, this is done by passing the {:first? true} parameter that enables the First reducer.

    (jdbc/on-connection [conn config]
      (jdbc/execute-one! conn ["select $1 as num" 42]))
    ;; {:num 42}
    

    To prepare a statement, pass a SQL-vector into the prepare function. The result will be an instance of the PreparedStatement class. To execute a statement, put it into a SQL-vector followed by the parameters:

    (jdbc/on-connection [conn config]
      (let [stmt
            (jdbc/prepare conn
                          ["select $1::int4 + 1 as num"])
            res1
            (jdbc/execute-one! conn [stmt 1])
    
            res2
            (jdbc/execute-one! conn [stmt 2])]
    
        [res1 res2]))
    
    ;; [{:num 2} {:num 3}]
    

    Above, the same stmt statement is executed twice with different parameters.

    More realistic example with inserting data into a table. Let’s prepare the table first:

    (jdbc/execute! config ["create table test2 (id serial primary key, name text not null)"])
    

    Insert a couple of rows returning the result:

    (jdbc/on-connection [conn config]
      (let [stmt
            (jdbc/prepare conn
                          ["insert into test2 (name) values ($1) returning *"])
    
            res1
            (jdbc/execute-one! conn [stmt "Ivan"])
    
            res2
            (jdbc/execute-one! conn [stmt "Huan"])]
    
        [res1 res2]))
    
    ;; [{:name "Ivan", :id 1} {:name "Huan", :id 2}]
    

    As it was mentioned above, in Postgres, a prepared statement is always bound to a certain connection. Thus, use the prepare function only inside the on-connection macro to ensure that all the underlying database interaction is made within the same connection.

    Transactions

    The with-transaction macro wraps a block of code into a transaction. Before entering the block, the macro emits the BEGIN expression, and COMMIT afterwards, if there was no an exception. Should an exception pop up, the transaction gets rolled back with ROLLBACK, and the exception is re-thrown.

    The macro takes a binding symbol which a connection is bound to, a source, an a map of options. The standard Next.JDBC transaction options are supported, namely:

    • :isolation
    • :read-only
    • :rollback-only

    Here is an example of inserting a couple of rows in a transaction:

    (jdbc/on-connection [conn config]
    
      (let [stmt
            (jdbc/prepare conn
                          ["insert into test2 (name) values ($1) returning *"])]
    
        (jdbc/with-transaction [TX conn {:isolation :serializable
                                         :read-only false
                                         :rollback-only false}]
    
          (let [res1
                (jdbc/execute-one! conn [stmt "Snip"])
    
                res2
                (jdbc/execute-one! conn [stmt "Snap"])]
    
            [res1 res2]))))
    
    ;; [{:name "Snip", :id 3} {:name "Snap", :id 4}]
    

    The Postgres log:

    BEGIN
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    insert into test2 (name) values ($1) returning *
      $1 = 'Snip'
    insert into test2 (name) values ($1) returning *
      $1 = 'Snap'
    COMMIT
    

    The :isolation parameter might be one of the following:

    • :read-uncommitted
    • :read-committed
    • :repeatable-read
    • :serializable

    To know more about transaction isolation, refer to the official [Postgres documentation][transaction-iso].

    When read-only is true, any mutable query will trigger an error response from Postgres:

    (jdbc/with-transaction [TX config {:read-only true}]
      (jdbc/execute! TX ["delete from test2"]))
    
    ;; Execution error (PGErrorResponse) at org.pg.Accum/maybeThrowError (Accum.java:207).
    ;; Server error response: {severity=ERROR, message=cannot execute DELETE in a read-only transaction, verbosity=ERROR}
    

    When :rollback-only is true, the transaction gets rolled back even there was no an exception. This is useful for tests and experiments:

    (jdbc/with-transaction [TX config {:rollback-only true}]
      (jdbc/execute! TX ["delete from test2"]))
    

    The logs:

    statement: BEGIN
    execute s1/p2: delete from test2
    statement: ROLLBACK
    

    The table still has its data:

    (jdbc/execute! config ["select * from test2"])
    
    ;; [{:name "Ivan", :id 1} ...]
    

    The function active-tx? helps to determine if you’re in the middle of a transaction:

    (jdbc/on-connection [conn config]
      (let [res1 (jdbc/active-tx? conn)]
        (jdbc/with-transaction [TX conn]
          (let [res2 (jdbc/active-tx? TX)]
            [res1 res2]))))
    
    ;; [false true]
    

    It returns true for transactions tha are in the error state as well.

    Keys and Namespaces

    The pg.jdbc wrapper tries to mimic Next.JDBC and thus uses kebab-case-keys when building maps:

    (jdbc/on-connection [conn config]
      (jdbc/execute-one! conn ["select 42 as the_answer"]))
    
    ;; {:the-answer 42}
    

    To change that behaviour and use snake_case_keys, pass the {:kebab-keys? false} option map:

    (jdbc/on-connection [conn config]
      (jdbc/execute-one! conn
                         ["select 42 as the_answer"]
                         {:kebab-keys? false}))
    
    ;; {:the_answer 42}
    

    By default, Next.JDBC returns full-qualified keys where namespaces are table names, for example :user/profile-id or :order/created-at. At the moment, namespaces are not supported by the wrapper.

    For more information, please refer to the official README file.

  • PG2 release 0.1.2: more performance, benchmarks, part 3

    Table of Content

    Introduction

    The PG2 library version 0.1.2 is out. One of its features is a significant performance boost when processing SELECT queries. The more fields and rows you have in a result, the faster is the processing. Here is a chart that measures a query with a single column:

    No difference between the previous release of PG and the new one. But with nine fields, the average execution time is less now:

    Briefly, PG2 0.1.2 allows you to fetch the data 7-8 times faster than Next.JDBC does. But before we proceed with other charts and numbers, let me explain how the new processing algorithm works.

    Read more →

  • Gzip

    Коллеги, используйте gzip! Это простой способ уменьшить трафик в разы, если не на порядок. Буквально двумя строчками можно превратить гигабайты CSV в 200-300 мегабайтов. Разве не чудо? И делается это парой строк.

    Теперь подробней. Gzip — старый алгоритм потокового сжатия. Ключевое слово “потоковый”. Это значит, алгоритму не нужен файл целиком; он читает окно байтов и выдает сжатое окно. За счет этого можно пережать любой поток, в том числе бесконечный.

    В джаве потоки байтов используют часто. В ней легко втиснуть GzipInputStream или GzipOutputStream, чтобы закодировать или декодировать поток. Например, если источник сжат Gzip, то обернем его так:

    (-> "some.file.gzip"
        (io/file)
        (io/input-stream)
        (GzipInputStream.))
    

    При чтении получим нормальный текст. А чтобы закодировать поток, делаем иначе: навесим на выходной поток GzipOutputStream и колбасим в него. Только в конце надо вызвать .finish, чтобы добить незавершенное окно.

    (let [out (-> "myfile.out.gzip"
                  io/file
                  io/input-stream)
          gzip (new GzipOutputStream out)]
      (while ...
        (.write gzip <bytes>))
      (.finish gzip))
    

    Удивляет, что при всей банальности gzip используют мало. А ведь он отлично подходит для текстовых данных: HTML, JSON, CSS, JS, CSV. В текущем проекте сервисы гоняют гигабайты CSV и JSON, и хоть кто-нибудь подумал о сжатии…

    Простой эксперимент: несжатый CSV — 146 мегабайтов, сжатый — 26. Почти в шесть раз. Даже если закодировать результат в base64, это даст +30% от 26 мегабайтов, то есть всего 35. Выгода все равно 4 раза. В том же Nginx сжатие gzip включается одной строкой в конфиге.

    Еще больше мою веру укрепил крит на проде. Один из сервисов выплюнул 6 мегабайтов JSON, что не помещается в квоту AWS Lambda. Пришлось в спешке прикручивать сжатие, чтобы сообщенька пролезла.

    Соответственно, бесят HTTP-клиенты, которые ничего не знают о Gzip. Работа с ними превращается в ад: сам проверь заголовки, сам закодируй-раскодируй… и в проекте используется именно такой! Он читает ответ как строку, не проверяя Content-Encoding, и если там gzip, получается чешуя. Авторам — большой ай-ай за игнор веб-стандартов.

    Чем раньше вы возьмете gzip в проект, тем лучше. Потом все равно придется, но будет больно.

  • Женские истории

    Когда читаю женские истории — Твиттер, блоги, журналистику, — вижу один и тот же паттерн:

    • он мне не нравился, но я осталась на свидании;
    • он мне не нравился, но мы целовались;
    • он мне не нравился, но мы поехали к нему;
    • он мне не нравился, но у нас был секс.

    То есть он всю дорогу не нравился, но программа шла по плану — от кафе до постели.

    Тут два момента: либо женщина и правда не знает, что если человек не нравится, то и секс с ним будет плохим. Надеюсь, теперь она это знает.

    Либо женщина все знает, но понимает, что в данный момент на мужчину лучше она рассчитывать не может. Поэтому все, что случилось, выставляется как одолжение с ее стороны.

    Чем раньше женщина прервет эту цепочку, тем лучше.

  • Изображая ответственность

    Я уже писал про известный шаблон в кино. Ученый делает бомбу годами, хотя даже грубые расчеты показывают, что она убьет тысячи людей. Ученый курит сигары, не спит, в голове красочные приходы.

    И вот бомба готова, ее забирают вояки, чтобы взорвать над городом. Гибнет двести тысяч человек. ВНЕЗАПНО ученый чувствует ответственность. Пока не умерли эти двести тысяч, кто сгорев, кто разлагаясь от лучевой болезни, ответственности не было. А тут появилась. Пропали сон и аппетит, начинаются походы в высшие инстанции: господин Президент, я чувствую ответственность.

    Словом, бред. Лети в Японию и расскажи погибшим про свою ответственность.

    Почему это нормально для фильма, ясно: историю всегда натягивают на драму. Неважно, про что фильм: бомба, война, политика — на первом месте стоит личная драма, иначе зрители не пойдут. Кроме того, зрителя подводит время. Два года создания бомбы сжаты до 40 минут, и для нас с вами она появляется внезапно: еще пять минут назад не было, а сегодня уже на стенде.

    К фильмам у меня нет претензий за этот штамп. Сняли и ладно. Но раздражает, когда псевдо-ответственность появляется в реальной жизни.

    Скажем, один из разработчиков Постгреса разразился плачем: жаль, что лицензия не позволяет запретить Постгрес в России. Ведь наверняка их армия применяет нашу базу. Срач вышел знатный, и даже пришел основатель проекта, чтобы унять активиста.

    Я не имею отношения к российской армии, но уверен: там используется и Постгрес, и Мария, и Питон, и Перл, и Плюсы и тысячи протоколов и стеков. Ровно как и в другой армии мира. Но если ты хочешь запретить, то где точность? Программист должен мыслить точно. Запрещай конкретно армии России. Почему под запрет попадает детский сад, где сервер 1С крутится на Постгресе?

    Далее, как быть с евреями, которые еще месяц назад обрушали дома с палестинскими детьми? Как-то не очень гуманно. Будем отзывать лицензии у всех фирм Израиля?

    Когда Китай скажет “Тайвань наш” и начнет свою “спецоперацию”, будем отзывать Постгрес у Китая? А потом возвращать по итогам переговоров?

    Еще есть плохие парни вроде торговцев оружием, наркотиками и CP. Свои дела они тоже хранят в базах данных, запрещай ты им или нет.

    Если думать об этом системно, задача становится слишком сложной. Кто-то должен трекать новости и решать, у кого сегодня мы отзываем, а кому возвращаем. Кто будет этим заниматься? Разработчик? Останется ли у него время на разработку после этих вопросов? Если поручить это секретарю, где гарантия, что он правильно истолкует повестку дня?

    Все потому, что с разработчиком случился казус из заголовка. ВНЕЗАПНО он почувствовал ответственность. Захотелось решать, кому можно пользоваться базой, а кому нельзя.

    Похожая история была с другим разработчиком, как ни странно, тоже связанным с Постгресом. У него на сайте был хороший парсер EXPLAIN ANALYSE, но затем автор закрыл доступ из России и Беларуси. Его спросили: зачем? Он накатал телегу про танки в Чехословакии. Так запрети доступ танковым войскам РФ. Зачем всем запрещаешь? Какую проблему ты пытаешься решить?

    Казалось бы, программисты — люди с точным мышлением, а в жизни все наоборот: в голове кисель.

    Для меня выход из ситуации простой: трусы или крестик. Либо ты выходишь из разработки со словами: хорош, я в этом не участвую. Либо пишешь качественный код, понимая, что мир огромен, и у тебя нет возможности контролировать, кто и как его использует. Оба варианта честные и снимают все вопросы.

    Вариант с псевдо-ответственностью лжив от начала до конца. Он ситуативен и основан на реакции, в нем нет идеи и своей повестки. Это желание попасть в тренд: Гугл-Эпл запрещают, а я чем хуже?

    Если вы хороший разработчик, прошу вас, не становитесь плохим Оппенгеймером. Косплей на великого человека будет крайне неудачным.

    Поэтому: не ищите ответственности там, где ее нет. Просто делайте то, что вы делаете хорошо. Соблюдать этот принцип несмотря ни на что — уже большая ответственность.

  • Вложенность

    Об этом никто не пишет, а ведь проблема серьезная. Я говорю о лишней вложенности данных. Недавний пример:

    {:manager
     {:risk
      {:time "10:30"
       :task-id 100400}}
     :accounter
     {:business
      {:time "12:35"
       :task-id 100500}}}
    

    Это вложенная мапа: на первом уровне роль менеджера, на втором тип апрува, на третьем — детали апрува: дата и ссылка на задачу. Спрашивается, зачем было группировать события по роли и типу? Какой смысл? Почему не записать два события:

    [{:role :manager
      :type :risk
      :time "10:30"
      :task-id 100400}
     {:role :accounter
      :type :business
      :time "12:35"
      :task-id 100500}]
    

    Плоско, понятно, нет вложенности. Кому надо, сгруппируют и по роли, и по дате, и бог знает как еще. А когда сгруппировано до нас, сперва нужно распутать, а потом группировать как надо.

    Второй пример: есть мапа с ключом children, в которой вектор мап. В каждой мапе — айдишка и имя:

    {:children
     [{:id 1 :name "test1"}
      {:id 2 :name "test2"}
      {:id 3 :name "test3"}]}
    

    Нужно построить индекс id => name, чтобы потом по нему искать. Я быстренько прошел по children и собрал словарь:

    {1 "test2"
     2 "test2"
     3 "test3"}
    

    На проде — сплошные промахи мимо индекса. Что такое? Оказалось, не до конца прокрутил файлик. У некоторых children есть свои children, то есть структура такая:

    {:children
     [{:id 1 :name "test1"}
      {:id 2 :name "test2"}
      {:id 3 :name "test3"}
      {:id 4
       :name "test4"
       :children
       [{:id 5
         :name "test5"}
        {:id 6
         :name "test6"
         :children
         [...]}]}]}
    

    При этом ограничения в глубину нет: может быть два уровня, может двадцать.

    Вот опять: кто этот гений, который сгруппировал? Как ты будешь это обходить? Ладно у меня Кложа, я написал (tree-seq :children :children data) и готово. А на каком-нибудь Питоне или Джаве голову сломаешь – тут дерево с обходом, нужен стек или очередь. Алгоритмы!

    Сделай ты плоский список и добавь parent_id. Кому надо, построит иерархию:

    [{:id 1 :name "test1"}
     {:id 2 :name "test2"}
     {:id 3 :name "test3"}
     {:id 4 :name "test4"}
     {:id 5 :name "test5" :parent-id 4}
     {:id 6 :name "test6" :parent-id 5}]
    

    И такое постоянно. Беру любой JSON и вижу, как его можно упростить, убрав лишнюю вложенность. Вдвойне обидно, что на эту вложенность кто-то тратил время, а она не нужна!

    Особенно я не люблю, когда вкладывают равнозначные сущности. Например, у нас два автора и у каждого две книги. JSON выглядит так:

    [{:id 1
      :name "John Smith"
      :books
      [{:id 20
        :title "The Best Novels"}
       {:id 30
        :title "Poems"}]}
     {:id 2
      :name "Sam Doe"
      :books
      [{:id 40
        :title "Some Story"}
       {:id 50
        :title "Achieves"}]}]
    

    Что если нужны не книги по авторам, а просто все книги? Опять ходи собирай.

    Я придумал способ как бороться такими случаями. О нем — в следующий раз.

  • Поза лотоса

    Когда я вижу программиста в позе лотоса, в голове играет сюжет.

    К автору картинки приезжает особая команда. Обученные люди сажают автора в позу лотоса и дают ноутбук. Его заставляют работать в этой позе час. За попытку встать, размяться, подвигать шеей — избиение ноутбуком по голове, а отсчет времени сбрасывается.

    Глядишь, так дизайнеры отучатся рисовать программиста в позе лотоса!

  • Мальчик и птица

    Я знал, что этого делать не следует, но сделал — сходил на мульт “Мальчик и птица”. Будет много скепсиса и спойлеров, я предупреждал.

    Писать о мультике тяжело. Хаяо Миядзаки — гений и наше все, поэтому любая критика в его адрес — что карикатуры на Мухаммеда: порвут на части. Я не буду критиковать мастера, а только опишу ощущения от просмотра.

    Начнем с того, что фильму не повезло с названием. В оригинале он называется “Как поживаете?” — это заголовок книги, по которой он снят. Однако связи между сюжетом и названием нет. Лишь один раз герой находит книгу с таким заголовком и благополучно забывает о ней. Кто и как поживает — не ясно.

    В американском прокате мульт назвали “The boy and the heron” — мальчик и цапля. Это правильно, потому что мальчика и (псевдо)цаплю мы видим большую часть фильма. И хотя у слова “heron” однозначный перевод — цапля, Карл!, — в русском прокате она стала птицей. Эта претензия не к Миядзаки, а к переводчикам.

    В целом, мульт страдает от тех же проблем, что и второй Аватар. При запредельном качестве картинки в нем никакой сюжет, все скомкано и ситуативно. Зрителя отвлекает миллион незначащих деталей. Досадно, что бюджет спустили на визуальную красоту, попустившись смыслом.

    Мульт идет 2 часа 10 минут — не так уж и мало. Первые 40 минут — это топтание на тему “не ходи в запретное место”. Понятно, что когда ребенку говорят не ходить куда-то, он пойдет, вопрос в том, как скоро. Но с этим большая затяжка.

    Когда герой входит в запретное место, начинается фантасмагория. Каждые десять минут он проваливается в новый мир со своими причудами. Мелькают локации, существа, загадочные сцены. Все это красиво, но требует ответов: кто это, откуда, зачем и что если? Режиссер не считает нужным все это объяснять; он забрасывает героя в новую локацию, где другие существа, враги и друзья. Словно летишь на карусели и видишь только мелькание.

    Иногда режиссер дает отдохнуть: показывает пять минут, как девушка режет хлеб, мажет маслом и ест. Ме-е-едленно и основа-а-ательно. Без музыки, только звон посуды и скрип пола. Красиво, но опять же — назрел миллион вопросов, может, объясните хоть что-нибудь? А бутерброд потерпит.

    Если вы думаете, что я цепляюсь зря, то вот малая толика вопросов:

    • Зачем герой рассек голову камнем?
    • Зачем мачеха пошла в башню?
    • Почему нельзя входить в родильную комнату?
    • Почему мачеха гонит героя прочь?
    • Какую роль играл нерожденный ребенок? Его хотели забрать? Кто и зачем?
    • Какую цель преследовал король попугаев?
    • Что за камни, из которых колдун строил башню? Почему они злые?
    • Что за хранитель гробницы? Почему его не показали?
    • Это все один мир или разные?
    • Если парень в мире мертвых — он умер?
    • Мать парня умерла в больнице или вознеслась на небо, потому что была огненным духом?
    • Что стало в параллельном мире после апокалипсиса?
    • Куда делся дед-колдун? Кто унаследовал его?

    Этих вопросов я могу написать пару экранов, но не вижу смысла. Спрашиваю вас: может быть, из всей этой каши надо было выбрать что-то одно и хорошенько его проработать? А не вываливать на голову, как тарелку с лапшой.

    Как я уже говорил, сюжету не хватает целостности: непонятно, какую проблему решает герой. Он отправился в башню, чтобы спасти мачеху, однако:

    • на чердаке он находит послание умершей матери — книгу “Как поживаете?”. Что там написано и какую роль играет находка — не ясно.

    • В мире мертвых живут забавные пузыри, которые улетают и становятся людьми. Этих пузырей жрут пеликаны. Показана сцена, где они слопали почти все пузыри. Как это относится к мачехе? Мы ее спасаем или нерожденных людей?

    • В мире мертвых живет женщина-рыболов, при этом она вовсе не мертвая. Что она там делает и зачем помогает герою — непонятно.

    • В другом мире живет горящая девочка, которую приносят в жертву главному колдуну. Намекается, что девочка — сестра мачехи и мать героя в мире людей. Как все это связано, можно только гадать.

    В сюжете напутаны сложные отношения: смущение подростка перед молодой мачехой, отстранение от отца, наставничество колдуна. Все это эпизодически, ситуативно, без малейшей проработки.

    Пожалуй, главное: нет морали. Совершенно не ясно, от чего предостерегает режиссер. Как жить, чтобы это не повторилось? Или наоборот: все, что случилось — хорошо? Где ориентиры?

    Изумила пара сцен:

    • отец дает беременной жене чемодан со словами “осторожно, он тяжелый”. После чего смотрит, как она тащит его в коляску. Помочь беременной жене? Не слышали.

    • Парнишка бьет себя камнем по голове, из нее вытекает ЛИТР крови. Парень спокойно идет домой. Хотел бы я это видеть. Нельзя было как-то реалистичней? Как писал выше, тема самонаказания не раскрыта.

    Словом, если вы смотрели другие мульты Миядзаки, сходите на “Мальчика и птицу” из чувства долга: все-таки прощальный мульт. Недочеты можно списать на фирменный стиль автора: кроме него таких мультов (почти) никто не делает. В отрыве от остального творчества Миядзаки мульт затянут и скучноват. Очень на любителя.

    PS: если речь пошла об аниме, в следующем посте расскажу о другом мульте, который, напротив, советую посмотреть.

  • PG2 benchmarks, part 2

    Table of Content

    In the previous post, I was measuring bare query/execute/copy functions of the library. Although it’s useful, it doesn’t render the whole picture because it’s unclear how an application will benefit from faster DB access.

    This post covers the second group of benchmarks I made: a simple HTTP server that reads random data from the database and responds with JSON. The benchmark compares PG2 and Next.JDBC as before.

    Introduction

    Some general notes: the server uses Ring Jetty version 1.7.1, JVM 21, Ring-JSON 0.5.1 for middleware. The handles are synchronous. The application uses connection pools for both libraries, and the pools are opened in advance when the server gets started. The maximum allowed pool size is 64. HTTP requests are sent by the ab utility with different -n and -c keys; the -l flag is always sent.

    Read more →

Страница 1 из 71