{"id":17237789,"url":"https://github.com/tarao/bullet-scala","last_synced_at":"2025-03-22T18:34:20.254Z","repository":{"id":36230017,"uuid":"40534336","full_name":"tarao/bullet-scala","owner":"tarao","description":"A monadic library to resolve object relations with the aim of avoiding the N+1 query problem.","archived":false,"fork":false,"pushed_at":"2015-08-15T07:59:12.000Z","size":176,"stargazers_count":54,"open_issues_count":0,"forks_count":3,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-01T17:51:30.444Z","etag":null,"topics":["monad","scala"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tarao.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-08-11T09:49:31.000Z","updated_at":"2023-04-18T21:15:26.000Z","dependencies_parsed_at":"2022-09-05T18:50:45.276Z","dependency_job_id":null,"html_url":"https://github.com/tarao/bullet-scala","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tarao%2Fbullet-scala","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tarao%2Fbullet-scala/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tarao%2Fbullet-scala/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tarao%2Fbullet-scala/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tarao","download_url":"https://codeload.github.com/tarao/bullet-scala/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244236060,"owners_count":20420753,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["monad","scala"],"created_at":"2024-10-15T05:43:45.680Z","updated_at":"2025-03-22T18:34:19.836Z","avatar_url":"https://github.com/tarao.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"bullet [![Build Status][travis-img]][travis] [![Coverage status][coverage-img]][coverage] [![Maven Central][maven-img]][maven] [![Scaladoc][javadoc-img]][javadoc]\n======\n\nA monadic library to resolve object relations with the aim of avoiding\nthe N+1 query problem.  The solution requires only pure computations\nin Scala depending on neither database implementations nor any other\nframeworks.\n\n## Getting started \u003ca name=\"install\"\u003e\u003c/a\u003e\n\nAdd dependency in your `build.sbt` as the following.\n\n```scala\n    libraryDependencies ++= Seq(\n      \"com.github.tarao\" %% \"bullet\" % \"0.0.2\"\n    )\n```\n\nThe library is available on [Maven Central][maven].  Currently,\nsupported Scala version is 2.11.\n\n## Overview \u003ca name=\"overview\"\u003e\u003c/a\u003e\n\n### The problem\n\nAssume that you have `Car` and `Engine` classes and, if `Car` has an\n`Engine`, it is resolved by a type class method `toEngine`.\n\n```scala\ntype CarId = Long\ntype EngineId = Long\n\ncase class Car(id: CarId)\ncase class Engine(id: EngineId, carId: CarId)\n\nimplicit class CarRelation(val car: Car) extends AnyVal {\n  def toEngine: Option[Engine] = ...\n}\n```\n\nIt is quite usual to implement `toEngine` method by using a repository\nwhich issues a DB query.\n\n```scala\nimplicit class CarRelation(val car: Car) extends AnyVal {\n  def toEngine: Option[Engine] = EngineRepository.findByCarId(car.id)\n}\n\nval db = ...\nobject EngineRepository {\n  def findByCarId(carId: CarId): Option[Engine] = db.run {\n    sql\"SELECT * FROM engine WHERE car_id = $carId LIMIT 1\".as[Engine]\n  }.headOption\n}\n```\n\nThere is no problem when you resolve an `Engine` from a `Car`.  In\nthis case, a `SELECT` query is executed internally.\n\n```scala\nval car: Car = Car(1234L)\nval engine: Option[Engine] = car.toEngine\n// SELECT * FROM engine WHERE car_id = 1234 LIMIT 1\n```\n\nIf you have multiple `Car`s and want to get their `Engine`s, you may\nwant to write like this.\n\n```scala\nval cars: Seq[Car] = Seq(Car(1L), Car(2L), Car(3L), ...)\nval engines: Seq[Engine] = cars.map(_.toEngine).flatten\n// SELECT * FROM engine WHERE car_id = 1 LIMIT 1\n// SELECT * FROM engine WHERE car_id = 2 LIMIT 1\n// SELECT * FROM engine WHERE car_id = 3 LIMIT 1\n// ...\n```\n\nYes, it works.  But there is a problem that the `SELECT` query is\nexecuted for each `id` of the element of `cars`.  When you have\nhundreds or thousands of `cars`, it is likely to be a perfomance\nissue.\n\nOne way to solve this problem is to `JOIN` tables.  When you\ninstantiate `Car`s from `car` table in your DB, `engine` table might\nalso be `INNER JOIN`ed.  This is a quite common solution but not the\nbest one.  If you have for example `Wheel`s, a `Bumper`, and `Door`s\nfor a `Car`, you will soon need to `JOIN` them all but not all of them\nare needed every time.  You will have to write instantiation methods\nwith ugly `JOIN` queries for each combination of parts that you need.\n\nIdeally, it would be nice if the last expression executes a single\n`SELECT` query.\n\n```scala\nval engines: Seq[Engine] = cars.map(_.toEngine).flatten\n// SELECT * FROM engine WHERE car_id IN (1, 2, 3, ...)\n```\n\nIs this possible?  In Scala, yes, it is.\n\n### Our solution\n\nAll you have to do is to replace `toEngine` method to return an\ninstance of a monad created by `HasA.Monadic` with an instance of\n`HasA[Car, Engine]`, which describes how to resolve `Engine`s from\n`Car`s.\n\n```scala\nimport com.github.tarao.bullet.HasA\n\nimplicit class CarRelation(val car: Car) extends AnyVal {\n  def toEngine = HasA.Monadic(car, hasEngine)\n}\nval hasEngine: HasA[Car, Engine] = ...\n```\n\nThe usage of `toEngine` is quite the same except that (1) you have to\nwrite a type of return value (`Option[Engine]` or `Seq[Engine]` in\nthis case), (2) you don't need to `flatten` anymore.\n\n```scala\nimport com.github.tarao.bullet.Implicits._\n\nval car: Car = ...\nval engine: Option[Engine] = car.toEngine\n\nval cars: Seq[Car] = ...\nval engines: Seq[Engine] = cars.map(_.toEngine)\n```\n\nThe implementation of `hasEngine` should resolve `Engine`s from `Car`s\nin a single query.  `HasA[Car, Engine]` has an interface for that\nnamed `map()`, whose type is `Seq[Car] =\u003e Seq[Engine]`.  Then the\nimplementation whould be the following.\n\n```scala\nval hasEngine: HasA[Car, Engine] = new HasA[Car, Engine] {\n  def map(cars: Seq[Car]): Seq[Engine] = db.run {\n    sql\"SELECT * FROM engine WHERE car_id IN (${cars.map(_.id)})\".as[Engine]\n  }\n}\n```\n\nNote that `map()` method is used for resolving both `Option[Engine]`\nand `Seq[Engine]`.  In our example, `toEngine` results in executing a\n`SELECT`-`WHERE`-`IN` query in the both cases.\n\n```scala\nval car: Car = Car(1234L)\nval engine: Option[Engine] = car.toEngine\n// SELECT * FROM engine WHERE car_id IN (1234L)\n\nval cars: Seq[Car] = Seq(Car(1L), Car(2L), Car(3L), ...)\nval engines: Seq[Engine] = cars.map(_.toEngine)\n// SELECT * FROM engine WHERE car_id IN (1, 2, 3, ...)\n```\n\n### How does it work?\n\nThe key mechanism is a monad, which is a return value of `toEngine`.\nIn the last example, we receive a monad as a variable of type\n`Option[Engine]` or `Seq[Engine]`.  This is actually an implicit\nconversion.  If we make it explicit, the example looks like this.\n\n```scala\nval car: Car = ...\nval engine: Option[Engine] = car.toEngine.run\n\nval cars: Seq[Car] = ...\nval engines: Seq[Engine] = cars.map(_.toEngine).run\n```\n\nIt is `run()` which actually calls `HasA[].map()`.  Until then, the\ninvocation of `HasA[].map()` is postponed inside the monad.  If the\nreceiver of `run()` is a list of monads, it will organize them into an\nargument of a single invocation of `HasA[].map()`.  (You may wonder\nhow it is possible since each monad value has its own instance of\n`HasA[]`.  It is actually only the first one in the list to be used.)\n\n### Why is it a monad?\n\nThe above story does not describe the way using our monad as a monad.\nActually, it is not necessarily a monad as long as it is some kind of\na deferred object.  It is a monad just for convenience.\n\nLet's see an example.  Suppose that an `Engine` has its `Crankshaft`\nand there is a type class method `toCrankshaft` defined in the same\nway as `toEngine`.  If we don't have the monadic feature, we need to\nlook up a `Crankshaft` of a `Car` via an `Engine` in the way like\nthis.\n\n```scala\nval car: Car = ...\nval engine: Option[Engine] = car.toEngine\nval crankshaft: Option[Crankshaft] =\n  engine.map(_.toCrankshaft: Option[Crankshaft]).flatten\n```\n\nIf we use the monadic feature, it can be written like this.\n\n```scala\nval car: Car = ...\nval crankshaft: Option[Crankshaft] = for {\n  e \u003c- car.toEngine\n  c \u003c- e.toCrankshaft\n} yield(c)\n```\n\nThis is much easier to read especially when you need a complex\noperation on `e` and/or `c`.\n\n## Has-a relation \u003ca name=\"has-a\"\u003e\u003c/a\u003e\n\nAs you have seen in the [overview][], only things you have to do are\nto implement `HasA[].map()` and to provide a monad factory using it.\n\n### Summary\n\n- Extend `HasA[From, To]` and implement `map: Seq[From] =\u003e Seq[To]`.\n- Provide a monad factory which returns `HasA.Monadic(from, hasA)` where:\n    - `from` is an instance of `From`\n    - `hasA` is an instance of `HasA[From, To]`\n\n## Has-many relation \u003ca name=\"has-many\"\u003e\u003c/a\u003e\n\nYou can specify a list type as `To` type argument of `HasA[From, To]`.\nIn this case, you can resolve a single value as a `Seq[_]` instead of\n`Option[Seq[_]]`, or multiple values as a `Seq[_]` instead of\n`Seq[Seq[_]]`.  For example, if you have `HasA[Car, Seq[Wheel]]` and\n`toWheels` returns a monad, then these can be used as the following.\n\n```scala\nval car: Car = ...\nval wheels: Seq[Wheel] = car.toWheels\nval cars: Seq[Car] = ...\nval totalWheels: Seq[Wheel] = cars.map(_.toWheels)\n```\n\n### Summary\n\n- The same as `HasA[From, To]` but `To` is a list type\n- The result can be flattened automatically\n\n## Joining related objects \u003ca name=\"join\"\u003e\u003c/a\u003e\n\nSometimes you may want to merge related two objects into one.  For\nexample, if you have `Car`s and their `Engine`s, you may want to have\nvalues of `CarWithEngine`s where those are merged.  To do this, you\ncan use another interface `Join.Monadic` to define a type class method\n`withEngine`.\n\n```scala\nimport com.github.tarao.bullet.Join\n\ntype CarWithEngine = (Car, Engine)\n\nimplicit class CarRelation(val car: Car) extends AnyVal {\n  def withEngine = Join.Monadic(car, joinEngine)\n}\n\ntype JoinEngineToCar = Join[CarWithEngine, CarId, Car, Engine]\nval joinEngine: JoinEngineToCar = new JoinEngineToCar {\n  def map(cars: Seq[Car]): Seq[Engine] = ... // the same as HasA[Car, Engine]\n  def leftKey(car: Car): CarId = car.id\n  def rightKey(engine: Engine): CarId = engine.carId\n  def merge(car: Car, engine: Engine): CarWithEngine = (car, engine)\n}\n```\n\nThis time you have four methods to implement.  `map()`, `leftKey()`,\n`rightKey()` and `merge()`.  `map()` is the same as in\n`HasA[Car, Engine]`.  `leftKey()` and `rightKey()` provides how you\nassociate one object to another.  In this case, a `Car` and its\n`Engine` should share their `CarId`.  associated objects are passed to\n`merge()`.\n\nThe usage is quite similar to `toEngine`.\n\n```scala\nval car: Car = ...\nval enginedCar: Option[CarWithEngine] = car.withEngine\n\nval cars: Seq[Car] = ...\nval enginedCars: Seq[CarWithEngine] = cars.map(_.withEngine)\n```\n\n### Summary\n\n- Extend `Join[Result, Key, Left, Right]` and implement four methods:\n    - `map: Seq[Left] =\u003e Seq[Right]`\n    - `leftKey: Left =\u003e Key`\n    - `rightKey: Right =\u003e Key`\n    - `merge: (Left, Right) =\u003e Result`\n- Provide a monad factory which returns `Join.Monadic(left, join)` where:\n    - `left` is an instance of `Left`\n    - `join` is an instance of `Join[Result, Key, Left, Right]`\n\n## Default values \u003ca name=\"default\"\u003e\u003c/a\u003e\n\nAn `Option[]` return value of `run()` may be `None` if `HasA[].map()`\nreturns an empty list.  In this case, you can provide a default value\nto ensure having some value returned.  This is done by providing an\nimplicit value of `Monad.Default[]`.  If you provide a default value,\nthe return value can be received without being wrapped by `Option[]`.\nFor example, the following code defines a default value for an\n`Engine`.\n\n```scala\nimplicit val defaultEngine: Monad.Default[Engine] =\n  Monad.Default[Engine](Engine(0L, 0L))\n\nval car: Car = ...\nval engine: Engine = car.toEngine\n```\n\nIn this case, note that **the implicit value must be visible in the\nscope where the invocation of `run()` or the implicit conversion\noccurs**.\n\nFor `Join[]`, you should be careful that you have two choices of types\nto provide a default value, either `Result` or `Right` of\n`Join[Result, Key, Left, Right]`.  If you provide a default value for\n`Result`, then you will always get a value by `run()` on a single\nmonad but still get some values lacked by `run()` on multiple monads.\nYou should provide a default value for `Right` to avoid this.  In this\ntime, **the implicit value must be visible in the scope where the\ninvoction of `Join.Monadic()` occurs**.\n\n## An implicit conversion vs. an explicit run\n\nYou may think that resolving object relations on an implicit\nconversion is too aggressive.  There is a way not to allow implicit\nconversions and force explicit `run()`s instead.  Only you have to do\nis not to `import com.github.bullet.Implicits._`.  Other things will\nwork fine without this.\n\n## License \u003ca name=\"license\"\u003e\u003c/a\u003e\n\n- Copyright (C) INA Lintaro\n- MIT License\n\n[travis]: https://travis-ci.org/tarao/bullet-scala\n[travis-img]: https://img.shields.io/travis/tarao/bullet-scala.svg?branch=master\n[coverage]: https://coveralls.io/github/tarao/bullet-scala?branch=master\n[coverage-img]: https://coveralls.io/repos/tarao/bullet-scala/badge.svg?branch=master\u0026service=github\n[maven]: https://maven-badges.herokuapp.com/maven-central/com.github.tarao/bullet_2.11\n[maven-img]: https://maven-badges.herokuapp.com/maven-central/com.github.tarao/bullet_2.11/badge.svg\n[javadoc]: http://javadoc-badge.appspot.com/com.github.tarao/bullet_2.11\n[javadoc-img]: http://javadoc-badge.appspot.com/com.github.tarao/bullet_2.11.svg?label=scaladoc\n\n[overview]: #overview\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftarao%2Fbullet-scala","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftarao%2Fbullet-scala","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftarao%2Fbullet-scala/lists"}