Mapless logo

Mapless

Schema-less persistence for Smalltalk with support for multiple backends.

Obscenely simple object persistence

Simple

Low maintenance for real. No instance variables, no getters, and no setters — just saved models with your data.

Add data to a model →

Composable

Do you need to compose models? But of course you do! No mapping required — just add those (sub)models, save its children first, and you're good to go.

Compose models →

Portable

Shared API across backends. For example, start with SQLite and easily move to MongoDB or PostgreSQL while caching with Redis.

Data portability →

Saving Smalltalk objects in seconds

Install in Pharo

Metacello new
      baseline: 'Mapless';
      repository: 'github://sebastianconcept/Mapless:latest/src';
      load
Need to persist modeled data?

This is what happens when you do it with Mapless:

  1. Create a dedicated subclass for each type of data model.
  2. Know in advance the attributes they'll require.
  3. Create instance variables for these attributes.
  4. Add accessors for each of them.
  5. Careful map them so it all fits in the database.
  6. Patiently re-map them every time you need to change its design saying 'hi' to object-database mismatches.
  7. Actually save them.
  8. Profit.
How does it look like?

Let's say you want to store and retrieve instances of Task as Mapless models using MongoDB as backend.

In essence, you'll create a repository object to handle Task instances — saving, querying, updating, or deleting them.

Let's see the snippets for achieving these basic operations one by one.

Create the repository

There are two ways to access MongoDB with Mapless. MaplessStandaloneMongoPool for single server setups and MaplessMongoReplicaSetPool for clusters of replica sets.

The following one shows an example for accessing a local standalone MongoDB.

Create a repository to access a standalone MongoDB service

 
  "Create the repository object to use the 'TryingMapless' database.
  This repository is for a standalone MongoDB listening on localhost:27017" 
  databaseName := 'TryingMapless'. 
  repository := MaplessMongoRepository 
    for: databaseName 
    with: MaplessStandaloneMongoPool local. 
  
Model your data

Creating new instances of your data models is straightforward — treat them like any other object.

The only thing to remark here is that you do not need to create the mutator methods description: nor isComplete: in the Task class.

Create a new model

Mapless subclass: #Task
	instanceVariableNames: ''
	classVariableNames: ''
	category: 'YourApp'.

"Create a task instance"
task := Task new 
	description: 'Try Mapless'; 
	isComplete: false.
Storing

To store data, simply send the save message to any Mapless instance, and be automatically inserted or updated in the database.

The repository object features the do: method, accepting a block closure. This allows your Mapless object to understand the repository it should work with.

Alternatively, you can implement Task class>>getRepository to have the model provide the repository directly, enabling it to handle the save operation without relying on a closure from external sources.

Saving a model

"Saving the task object"
  repository do: [ task save ].

  "Saving the task object when its class implements getRepository"
  task save.
  
Querying

Mapless supports various persistent backends, and the method of implementing queries for your data depends on the specific backend in use. For instance, with a PostgreSQL or SQLite backend, SQL queries are employed. In the case of MongoDB, Mapless utilizes the MongoTalk querying feature, while Redis adopts a namespaced convention.

Fetching objects

  "Get all models of a given Mapless class from the repository"
    allTasks := repository findAll: Task.

    "Same within the repository context"
    repository do: [ allTasks := Task findAll ].

    "Same but when your Mapless class can do its own getRepository
    (uses the MaplessCurrentRepository DynamicVariable)"
    allTasks := Task findAll.

    "Getting a specific model using its id"
    thatTask := Task findId: 'c472099f-79b9-8ea2-d61f-e5df34b3ed06'.

    "The Mapless models that are stored in MongoDB can use MongoTalk querying features"
    thatTask := Task findOne: { #description -> 'Try Mapless' } asDictionary.
    
    "All tasks of a given user"
    allUserTasks := Task find: { #username -> 'tasksninja' } asDictionary.

    "Second batch of 10 tasks about trying something ordered by description"
    criteria := { #description -> { '$regex' -> '^Try.*' } asDictionary } asDictionary.
    userTasksAboutTryingThings := Task find: criteria
      limit: 10
      offset: 20
      sort: { #description -> 1 } asDictionary.   

    "If we'd be using PostgreSQL instead and wanting the same data"
    criteria := 'maplessData->> ''description'' LIKE ''Try%'''.
    
    "The criteria filter here is used in the predicate of the SQL sentence 
    that Mapless creates for querying this"
    userTasksAboutTryingThings := Task find: criteria
      limit: 10
      offset: 20
      sort: 'description DESC'.
    
Making the queries powerful

Let's say you are using a MongoDB backend and want a production ready query by username for this Task. All you have to do is to create an index in MongoDB for username in the Task collection. Optionally, you can add a class method in Task tailored to this query.

Querying efficiently

Task class>>findAtUsername: aString
        "Answer the Task with the given username or nil if there is no match for it."
        ^ self findOne: { #username -> aString } asDictionary.

  "Query objects using an indexed attribute"
  tasks := repository do: [ Task findAtUsername: 'tasksninja' ].
  
Adding properties to models

Simply do it. Unless overriden, Mapless catches all messages you send to a model via DNU. It is smart enough to allow you to add a date a string or almost any other object without declaring instVars or demanding making schemas or any accessors for it.

Adding information to your data

"Adds or updates values of the model properties, then saves"
repository do: [
    task 
        deadline: Date tomorrow; 
        notes: 'Wonder how it feels to use this thing, hmm...';
        save ].

"No, wait! we forgot to add this..."
repository do: [
    task 
        priority: #high;
        save ].

"...and this"
repository do: [
    task 
        tags: #('flexible' 'portable');
        save ].
Composition

When you model data using your custom Mapless classes, you have the flexibility to add almost any property to them, especially other Mapless models—hence, composition. In such cases, Mapless efficiently organizes the data, utilizing one collection or table in the backend per type of Mapless object.

The main rule when working with composition here is save the children first.

Composing models

 
"Create and save a new alert for the task, 
then add it to the task and save the task" 
  repository do: [ | anAlert | 
    
    "Create new alert and save it"
    alert := Alert new 
      duration: 24 hours;
      message: 'Get it done!'; 
      save. 

    "Update the task having that saved alert"
    task alert: alert. 
    task save ]. 
  
Graph navigation

Saving a Mapless object is akin to planting the root of a graph. When interacting with a composed Mapless object, accessing the child Mapless feels like navigating one level deeper in the object graph. Composed Mapless objects return lazy references for every known (sub)Mapless to the parent. When you send a message to a (sub)model, the DNU method seamlessly resolves it, making it appear as if it was always there.

At the same time, if a Mapless object never sends any message to a child, then your application is not doing any unnecessary query. This prevents your backend from doing unnecessary extra work in advance making your system more efficient by default and in general.

Navigating model graphs

"The alert property has an instance of MaplessReference"
repository do: [ task alert class = MaplessReference ].
"=> true"

"At this point, Mapless never queried to fetch the data that belongs to the alert of that task..."

"Alert as (sub)model in task, is lazily fetched with the first message 
and responding as if always would have been ready"
repository do: [ task alert message ].
"=> 'Get it done!'"
Supported backends

All Mapless subclasses share the same fundamental Smalltalk API. When resolving requested operations, they delegate to a corresponding MaplessRepository subclass aligned with one of the concrete supported backends.

Currently supported backends include:

  1. SQLite.
  2. PostgreSQL.
  3. MongoDB.
  4. Redis.
  5. Memory.
  6. UnQLite (Deprecated and retiring soon).
Why Mapless?
I wanted to persist objects with low friction, low maintenance but high scale, throughput and availability capabilities and Mapless is totally biased towards that. This framework is what I came up with after incorporating my SaaS experience with airflowing.
  • There are no instVars...
  • No accessors...
  • No object-mapping impedance...

  • — Only persistence.

  • What would you do with it?
Applicability and Real-World Scenarios

Mapless, in active production since 2014, has proven its versatility across diverse scenarios—from initial Proof of Concepts to supporting mission-critical services actively monitored 24/7 by numerous network operators in the telecom industry.

Mapless excels in various use cases, including:

  • Proof of Concept APIs: Rapidly develop and test APIs for conceptual exploration.
  • BBFs - Backend For Frontends: Serve as a robust backend solution tailored for frontend applications.
  • MVP Development: Facilitate the creation of Minimal Viable Products (MVPs) with minimal costs and unparalleled speed.
  • RESTful Custom APIs: Craft customized and efficient APIs adhering to RESTful principles.
  • Web Application and API Integration: Seamlessly integrate with external CRMs, facilitating lead generation for the hospitality industry in Austria.
  • Scalable APIs and networking microservices: Scaling APIs to thousands of operations per second for years in the North American Telecommunications industry.
Guides

Here are some guides to help you get started:

Frequently Asked Questions

What saving 'Models' means? why not any object?

By "Models," Mapless refers to instances that represent persistent data, excluding transient entities such as contexts, sockets, or file handlers. Any instance of a Mapless subclass can be saved effortlessly. These instances are serialized and stored as documents in their respective MongoDB collection or PostgreSQL table.

Is only for tree-like structures or does actually support an object graph?

As long as you adhere to its rules, such as saving children first and treating your models as NoSQL-friendly documents and not trying to store a Socket or some unserializable object, yes, Mapless does support an arbitrary object graph.

Why would I want to use Mapless?

Because you might want to profit from some of these benefits:

  1. JSON friendly persisted models.
  2. Having frictionless system interoperability with Ruby and NodeJS and any JSON friendly object oriented app.
  3. Dealing with Gigas or Teras order of magnitude databases.
  4. High availability via MongoDB Replica Set features or PostgreSQL Replication.
  5. Replication of the whole database across the cloud.
  6. World class databases many engineers are familiar to use and maintain.
  7. Powerful queries and custom indices.
  8. Freedom from a prioristic instVars declarations.
  9. Freedom from mappings maintenance when the design changes.
  10. Cross-image model caching (requires Redis).
  11. Cross-image model observation/reaction, horizontally scalling the Observer Pattern (requires Redis PUB/SUB).
All that with very low-friction and low-maintenance using one comfortable API.

Found a Bug?
Need that feature for your use case?

Please create a GitHub issue in the Mapless repository.

Need additional support?

Don't hesitate to reach out in 𝕏 or LinkedIn Pharo Discord server for discussing design possibilities and implementation support.

License

Since 2014 Sebastian Sastre published Mapless under the terms of MIT License.

Mapless source code in GitHub

Brought to you by Flowing

Since April 2014