Sprinkles is a boiler-plate-reduction-library for dealing with databases in android applications

Overview

Icon Sprinkles Build Status

Sprinkles is a boiler-plate-reduction-library for dealing with databases in android applications. Some would call it a kind of ORM but I don't see it that way. Sprinkles lets SQL do what it is good at, making complex queries. SQL however is a mess (in my opinion) when is comes to everything else. This is why sprinkles helps you with things such as inserting, updating, and destroying models. Sprinkles will also help you with the tedious task of unpacking a cursor into a model. Sprinkles actively supports version 2.3 of Android and above but it should work on older versions as well.

Download

Using gradle, add the following to your build.gradle. Just replace x.x.x with the correct version of the library (found under the releases tab).

dependencies {
    compile 'se.emilsjolander:sprinkles:x.x.x'
}

If you are not using gradle for whatever reason i suggest you clone the repository and check out the latest tag.

Getting started

When you have added the library to your project add a model class to it. I will demonstrate this with a Note.java class. I have omitted the import statements to keep it brief.

@Table("Notes")
public class Note extends Model {

    @Key
	@AutoIncrement
	@Column("id")
	private long id;

	@Column("title")
	public String title;

	@Column("body")
	public String body;

	public long getId() {
		return id;
	}

}

Ok, a lot of important stuff in this short class. First of all, a model must subclass se.emilsjolander.sprinkles.Model and it also must have a @Table annotations specifying the table name that the model corresponds to. After the class declaration we have declared three members: id, title and body. Notice how all of them have a @Column annotation to mark that they are not only a member of this class but also a column of the table that this class represents. We have one last annotation in the above example. The @AutoIncrement annotation tells sprinkles that the field should automatically be set upon the creation of its corresponding row in the table. Key columns are the columns that are used to decide whether a model is already stored in the database when using methods such as delete() and save().

Before using this class you must migrate it into the database. I recommend doing this in the onCreate() method of an Application subclass like this:

public class MyApplication extends Application {

	@Override
	public void onCreate() {
		super.onCreate();

		Sprinkles sprinkles = Sprinkles.init(getApplicationContext());

        sprinkles.addMigration(new Migration() {
            @Override
            protected void onPreMigrate() {
                // do nothing
            }

            @Override
            protected void doMigration(SQLiteDatabase db) {
                db.execSQL(
                        "CREATE TABLE Notes (" +
                                "id INTEGER PRIMARY KEY AUTOINCREMENT,"+
                                "title TEXT,"+
                                "body TEXT"+
                        ")"
                );
            }

            @Override
            protected void onPostMigrate() {
                // do nothing
            }
        });
	}

}

Now you can happily create new instances of this class and save it to the database like so:

public void saveStuff() {
	Note n = new Note();
	n.title = "Sprinkles is awesome!";
	n.body = "yup, sure is!";
	n.save(); // when this call finishes n.getId() will return a valid id
}

You can also query for this note like this:

public void queryStuff() {
	Note n = Query.one(Note.class, "select * from Notes where title=?", "Sprinkles is awesome!").get();
}

There is a lot more you can do with sprinkles so please read the next section which covers the whole API!

API

###Annotations

  • @Table Used to associate a model class with a SQL table.
  • @AutoIncrement Used to mark a field as an auto-incrementing. The field must be an int or a long.
  • @Column Used to associate a class field with a SQL column.
  • @DynamicColumn Used to associate a class field with a dynamic SQL column such as an alias in a query.
  • @Key Used to mark a field as a key. Multiple keys in a class are allowed and will result in a composite key. Keys will most often want to be mapped directly to primary keys in your database.

###Saving The save method is both an insert and an update method, the correct operation will be done depending on the model's existence in the database. The first two methods below are synchronous, the second is for use together with a transaction (more on that later). There are also two asynchronous methods, one with a callback and one without. The synchronous methods will return a boolean indicating if the model was saved or not. The asynchronous method with a callback will just not invoke the callback if saving failed.

boolean save();
boolean save(Transaction t);
void saveAsync();
void saveAsync(OnSavedCallback callback);

All the save methods use this method to check if a model exists in the database. You are free to use it as well.

boolean exists();

###Deleting Similar to saving there are four methods that let you delete a model. These work in the same way as save but will not return a boolean indicating the result.

void delete();
void delete(Transaction t);
void deleteAsync();
void deleteAsync(OnDeletedCallback callback);

###Querying Start a query with on of the following static methods:

Query.one(Class<? extends QueryResult> clazz, String sql, Object[] args);
Query.many(Class<? extends QueryResult> clazz, String sql, Object[] args);
Query.all(Class<? extends Model> clazz);

Notice that unlike androids built in query methods you can send in an array of objects instead of an array of strings.

Once the query has been started you can get the result with two different methods:

<T extends QueryResult> get();
boolean getAsync(LoaderManager lm, ResultHandler<? extends Model> handler, Class<? extends Model>... respondsToUpdatedOf);

get() returns either the QueryResult or a list of the QueryResult represented by the Class you sent in as the first argument to the query method. getAsync() is the same only that the result is delivered on a callback function after executing get() on another thread. getAsync() also delivers updated results once the backing model of the query is updated if you return true indicating you want further updates. getAsync() uses loaders and therefore needs a LoaderManager instance. getAsync() also takes an optional array of classes which is used when the query relies on more models than the one you are querying for and you want the query to be updated when those models change as well.

###CursorList All Queries return a CursorList subclass. This is a Iterable subclass which lazily unpacks a cursor into its corresponding model when you ask for the next item. This leads to having the efficiency of a Cursor but without the pain. Excluding the Iterable methods CursorList also provides the following methods.

public int size();
public T get(int pos);
public List<T> asList();

Remember to always call close() on a CursorList instance! This will close the underlying cursor.

###ModelList For mass saving/deletion of models you can use the ModelList class. It extends ArrayList and has the following additional methods:

public static <E extends Model> ModelList<E> from(CursorList<E> cursorList);
public boolean saveAll();
public boolean saveAll(Transaction t);
public void saveAllAsync();
public void saveAllAsync(OnAllSavedCallback callback);
public void deleteAll();
public void deleteAll(Transaction t);
public void deleteAllAsync();
public void deleteAllAsync(OnAllDeletedCallback callback);

from(CursorList<E extends Model> cursorList) is a helper method which creates a ModelList from a CursorList, so you can e.g. delete all models from a previous query in one batch. Be aware, that the cursor is not closed for you when calling this method and you have to do it yourself!

###Transactions Both save() and delete() methods exist which take in a Transaction. Here is a quick example on how to use them. If any exception is thrown while saving a model or if any model fails to save the transaction will be rolled back.

public void doTransaction(List<Note> notes) {
	Transaction t = new Transaction();
	try {
		for (Note n : notes) {
			if (!n.save(t)) {
				return;
			}
		}
		t.setSuccessful(true);
	} finally {
		t.finish();
	}
}

###Callbacks Each model subclass can override a couple of callbacks.

Use the following callback to ensure that your model is not saved in an invalid state.

@Override
public boolean isValid() {
	// check model validity
}

Use the following callback to update a variable before the model is created

@Override
protected void beforeCreate() {
	mCreatedAt = System.currentTimeMillis();
}

Use the following callback to update a variable before the model is saved. This is called directly before beforeCreate() if the model is saved for the first time.

@Override
protected void beforeSave() {
	mUpdatedAt = System.currentTimeMillis();
}

Use the following callback to clean up things related to the model but not stored in the database. Perhaps a file on the internal storage?

@Override
protected void afterDelete() {
	// clean up some things?
}

###Migrations Migrations are the way you add things to your database. I suggest putting all your migrations in the onCreate() method of a Application subclass. Here is a quick example of how that would look:

public class MyApplication extends Application {

	@Override
	public void onCreate() {
		super.onCreate();

		Sprinkles sprinkles = Sprinkles.init(getApplicationContext());

        sprinkles.addMigration(new Migration() {
            @Override
            protected void onPreMigrate() {
                // do nothing
            }

            @Override
            protected void doMigration(SQLiteDatabase db) {
                db.execSQL(
                        "CREATE TABLE Notes (" +
                                "id INTEGER PRIMARY KEY AUTOINCREMENT,"+
                                "title TEXT,"+
                                "body TEXT"+
                        ")"
                );
            }

            @Override
            protected void onPostMigrate() {
                // do nothing
            }
        });
	}

}

Migrations are performed using raw SQL, this allowes full freedom to use all of the powerfull contraints that are possible to put on columns. Two optional methods are provided that allow you do some form of processing of your data before and after a migration, this can be usefull when recreating a table with different properties but you want to keep the data that was previously stored in the now deleted table. Once a migration has been added with sprinkles.addMigration() it should NEVER be changed, and all new migrations should be added after the previous migration. This ensures both old and new clients will have a consistent database and you will not need to care about database versioning.

###Type serializers Through an instance of Sprinkles you can register your own TypeSerializer instances via registerType() for serializing an object in your model into a column in the database. Sprinkles uses a TypeSerializer implementation internally for all the different data types that it supports. So check out the se.emilsjolander.sprinkles.typeserializers package for example implementations. These serializers will be used both when saving a model and when querying rows from the database.

###ContentObservers Sprinkles supports ContentObservers for change notifications. By registering your models for observation you can ensure your ContentObserver will be notified of changes.

SprinklesContentObserver observer;
ContentObserver myCustomObserver = ...;

this.observer = new SprinklesContentObserver(myCustomObserver);

@Override
public void onResume() {
    super.onResume();
    this.observer.register(Note.class, true); // true/false for notify descendants
}

@Override
public void onPause() {
    super.onPause();
    this.observer.unregister();
}

###Relationships Sprinkles does nothing to handle relationships for you; this is by design. You will have to use the regular ways to handle relationships in SQL. Sprinkles gives you all the tools needed for this and it works very well.

Comments
  • Let the user choose a database name + Change numbering for DB Versions.

    Let the user choose a database name + Change numbering for DB Versions.

    The version number for a DB was based on the number of migrations. When you want to open an existing Database, you don't want to apply a migration, just open the DB. As the SQLiteOpenHelper starts to version at 1 and not 0, we increment the number of migrations by one.

    opened by fstephany 12
  • Automatically migrating models does not work well with new installs

    Automatically migrating models does not work well with new installs

    When doing migration.createTable() and then adding a column with migration.addColumn() it fails on new installs.

    There are two solutions. We could automatically track changes of models and migrate when necessary, this would probably be hard to integrate with custom migrations in a predictable way which would be the downside.

    The other solution would be to drop auto migrating of models and the user would need to manually migrate all columns. This would however cause the need for bigger changes as annotations currently made on columns would be unnecessary as those constraints should be added on the migration (which might be better than having to annotate properties?).

    bug discussion 
    opened by emilsjolander 9
  • Reintroduce ModelList for mass model save/delete actions

    Reintroduce ModelList for mass model save/delete actions

    I'm using your old ModelList class for mass save actions together with Retrofit. My Retrofit services return ModelList<? extends Model> types for which I then can do service.fetchAll().saveAll();.

    I've also added the static method from(CursorList<? extends Model> cursorList) which returns a new ModelList from a CursorList so that you can e.g. delete all models of your query with one call.

    opened by jdreesen 9
  • Why is the android:minSdkVersion=

    Why is the android:minSdkVersion="14"?

    Can you lower this to, say, 8? I can pull down the sources and modify it myself and do a local build but I'd like to avoid that, that is, if Sprinkles would even work at that level.

    opened by praneetloke 8
  • Raw sql execution

    Raw sql execution

    Support for raw sql execution. Something a long the lines of Sprinkles.exec(String sql, Object[] args) and Sprinkles.execAsync(String sql, Object[] args)

    enhancement 
    opened by emilsjolander 8
  • SyncAdapter support

    SyncAdapter support

    I would love to be able to hook into the callback in Model.save() in order to tell the underlying content provider to start a sync adapter. For example the change might look like...

    Sprinkles.sInstance.mContext.getContentResolver()
        .notifyChange(Utils.getNotificationUri(Model.this.getClass()), null, true);
    

    The extra true param on the end would allow for using a SyncAdapter alongside Sprinkles.

    I would be happy to implement this I'm just curious if @emilsjolander has any input on this.

    Thanks

    opened by zsiegel 7
  • reuse callbacks, default table/column names, & unique combinations

    reuse callbacks, default table/column names, & unique combinations

    The library is great but to integrate it into a project I am working on I needed to add unique combinations and that I could restart the loaders to working with callbacks more than once.

    While adding that I made it so that I could use the variable names and class names as the column names and table names respectively by default if no argument is given to @column or @table

    I hope that you and the rest of your library's users find these modifications useful

    opened by mechinn 7
  • Searching by Date

    Searching by Date

    Is sprinkles able to search by date?

    Updating the sample app:

    Query.many(Note.class,  "select Notes.*, " +
        "(select count(*) from NoteTagLinks where NoteTagLinks.note_id = Notes.id) as tag_count " +
        "from Notes where created_at = ?", new Date());
    

    This never returns any entries.

    opened by f2prateek 7
  • OneQuery.getAsync() returns null when OneQuery.get() doesn't

    OneQuery.getAsync() returns null when OneQuery.get() doesn't

    Hello world!

    I'm seeing some strange issues with Sprinkles when using it in my nested Fragments (in a FragmentStatePagerAdapter that I pass getChildFragmentManager() into).

    Consider the following code in onViewCreated:

    DetailedStatsDAO detailedStatsDAO = Query.one(
        DetailedStatsDAO.class,
            "SELECT * " +
            "FROM " + DetailedStatsDAO.TABLE_NAME + " " +
            "WHERE soldierId = ? AND platformId = ? AND version = ?",
            arguments.getString(Keys.Soldier.ID, ""),
            arguments.getInt(Keys.Soldier.PLATFORM, 0),
            BuildConfig.VERSION_CODE
        ).get();
    
        Log.d("HelloSprinkles", "[onViewCreated] detailedStatsDAO => " + detailedStatsDAO);
        sendDataToListView(view, detailedStatsDAO.getDetailedStats().getItems());
        showLoadingState(false);
    

    This populates the ListView as expected to, as statsDAO is an instance of DetailedStatsDao:

    D/HelloSprinkles﹕ [onViewCreated] detailedStatsDAO => DetailedStatsDAO@43c73408
    

    However, consider the same query and handling of result, but using getAsync() instead:

    Query.one(
        DetailedStatsDAO.class,
        "SELECT * " +
        "FROM " + DetailedStatsDAO.TABLE_NAME + " " +
        "WHERE soldierId = ? AND platformId = ? AND version = ?",
        arguments.getString(Keys.Soldier.ID, ""),
        arguments.getInt(Keys.Soldier.PLATFORM, 0),
        BuildConfig.VERSION_CODE
    ).getAsync(
        getLoaderManager(),
        new OneQuery.ResultHandler<DetailedStatsDAO>() {
            @Override
            public boolean handleResult(DetailedStatsDAO detailedStatsDAO) {
                Log.d("HelloWorld", "[getAsync] detailedStatsDAO => " + detailedStatsDAO);
                if (detailedStatsDAO == null) {
                    return true;
                }
    
                sendDataToListView(view, detailedStatsDAO.getDetailedStats().getItems());
                showLoadingState(false);
                return true;
            }
        }
    );
    

    This instead gives me a null value, for some non-apparent reason:

    D/HelloSprinkles﹕ [getAsync] detailedStatsDAO => null
    

    If I then swipe to the fragment in question, it gets passed an OK instance upon the fragment sliding into view:

    D/HelloSprinkles﹕ [getAsync] detailedStatsDAO => DetailedStatsDAO@449d0808
    

    ...and, as an added "whaaaaa...t" bonus, passing getFragmentManager() instead of getChildFragmentManager() in the parent fragment seems to get me the correct results using getAsync:

    D/HelloSprinkles﹕ [getAsync] detailedStatsDAO => DetailedStatsDAO@42d58fd8
    

    Any thoughts? I'm not sure I fully understand why things are going down the way they are, as I believe getAsync should be returning the same results as get() does.

    Thanks :-)

    opened by karllindmark 6
  • Use existing database

    Use existing database

    I would like to use Sprinkles with an existing database. Right now, the database name is hardcoded in DbOpenHelper. It would be great to be able to open a database with another name bundled in the App.

    opened by fstephany 6
  • Returning false in the callback for getAsync() still calls the callback

    Returning false in the callback for getAsync() still calls the callback

    OK. I'll try my best to describe this correctly. Right now, I am not sure if I am doing something wrong or if there is a bug in the async callback.

    I have a scenario where I check for the existence of a certain item in the DB before I save it and hence I have a Query.One().getAsync() call. The callback given to the getAsync() part is returning false and yet it gets called again when I actually do save the item if it doesn't exist in the database. Moreover, since I save to the DB using saveAsync(), Sprinkles seems to call the callback anyway even when the record has not yet been saved and, thereby resulting in two records in the DB for a single save.

    I guess a simple code sample to illustrate would be clearer:

    private OneQuery.ResultHandler<Example> onExampleLoaded = new OneQuery.ResultHandler<Example>() {
            @Override
            public boolean handleResult (Example example) {
                if (example == null) {
                    String something = "hello";
    
                    Example newExample = new Example(something);
                    newExample.saveAsync(new Model.OnSavedCallback() {
                        @Override
                        public void onSaved () {
                            Log.i(TAG, "Saved"); //this executes twice
                        }
                    });
                }
                //don't want to be notified of further changes
                return false;
            }
        };
    
    opened by praneetloke 6
  • Improve GRADLE build Performance

    Improve GRADLE build Performance

    Parallel builds. This project contains multiple modules. Parallel builds can improve the build speed by executing tasks in parallel. We can enable this feature by setting org.gradle.parallel=true.

    Configuration on demand. Configuration on demand tells Gradle to configure modules that only are relevant to the requested tasks instead of configuring all of them. We can enable this feature by setting org.gradle.configureondemand=true.

    gradle caching. Shared caches can reduce the number of tasks you need to execute by reusing outputs already generated elsewhere. This can significantly decrease build times. We can enable this feature by setting org.gradle.caching=true.

    Gradle daemon. The Daemon is a long-lived process that help to avoid the cost of JVM startup for every build. Since Gradle 3.0, Gradle daemon is enabled by default. For an older version, you should enable it by setting org.gradle.daemon=true.

    ===================== If there are any inappropriate modifications in this PR, please give me a reply and I will change them.

    opened by hongdongni 0
  • Android Layout change Error

    Android Layout change Error

    Hi Everybody. I developed one project. This project is run good in the debug mode. But this project is changed in layout(LinearLayout) when I build the release version. How can I fix this error? Thank you for your replying. :)

    opened by stanicavuleta 0
  • save() method not saving datas on db in Android oreo.

    save() method not saving datas on db in Android oreo.

    As per the new changes in Oreo, The library seems to be not working well. The save() method returns an exception at line 138 of Model.class.

    Sprinkles.sInstance.mContext.getContentResolver().notifyChange( Utils.getNotificationUri(Model.this.getClass()), null, true);

    This might be because of the new changes in ContentProvider class on Android Oreo. From the Android doc, it is clear that content provider must be declared if we deal with the local databases. A content provider class declaration need from the library in an urgent hotfix release. @fstephany @jdreesen @fstephany @vinc3m1 @danielohrlund @f2prateek @emilsjolander

    opened by athulantonynp 2
  • Fix broken headings in Markdown files

    Fix broken headings in Markdown files

    GitHub changed the way Markdown headings are parsed, so this change fixes it.

    See bryant1410/readmesfix for more information.

    Tackles bryant1410/readmesfix#1

    opened by bryant1410 0
Releases(1.3.1)
Owner
Emil Sjölander
Emil Sjölander
Kotlin-Exposed-SQL - Example of using Exposed with Kotlin for the consumption of relational SQL Databases

Kotlin Exposed SQL Sencillo ejemplo sobre el uso y abuso de Exposed ORM de Jetbr

José Luis González Sánchez 3 Jun 14, 2022
ORMDroid is a simple ORM persistence framework for your Android applications.

ORMDroid is a simple ORM persistence framework for your Android applications, providing an easy to use, almost-zero-config way to handle model persist

Ross Bamford 87 Nov 10, 2022
A blazing fast, powerful, and very simple ORM android database library that writes database code for you.

README DBFlow is fast, efficient, and feature-rich Kotlin database library built on SQLite for Android. DBFlow utilizes annotation processing to gener

Andrew Grosner 4.9k Dec 30, 2022
A blazing fast, powerful, and very simple ORM android database library that writes database code for you.

README DBFlow is fast, efficient, and feature-rich Kotlin database library built on SQLite for Android. DBFlow utilizes annotation processing to gener

Andrew Grosner 4.9k Dec 30, 2022
An Android library that makes developers use SQLite database extremely easy.

LitePal for Android 中文文档 LitePal is an open source Android library that allows developers to use SQLite database extremely easy. You can finish most o

Lin Guo 7.9k Dec 31, 2022
SquiDB is a SQLite database library for Android and iOS

Most ongoing development is currently taking place on the dev_4.0 branch. Click here to see the latest changes and try out the 4.0 beta. Introducing S

Yahoo 1.3k Dec 26, 2022
Android library for auto generating SQL schema and Content provider

Android-AnnotatedSQL Android library for auto generating SQL schema and Content Provider by annotations. You will get a full-featured content provider

Gennadiy Dubina 161 Dec 3, 2022
A Java/Kotlin library for Android platform, to manage bean's persistence in SQLite, SharedPreferences, JSON, XML, Properties, Yaml, CBOR.

Thanks to JetBrains for support Kripton Persistence Library project! Kripton Persistence Library Kripton is a java library, for Android platform, that

xcesco 117 Nov 11, 2022
An Android library that makes developers use SQLite database extremely easy.

LitePal for Android 中文文档 LitePal is an open source Android library that allows developers to use SQLite database extremely easy. You can finish most o

Lin Guo 7.9k Jan 4, 2023
LiteGo is a Java-based asynchronous concurrency library. It has a smart executor, which can be freely set the maximum number of concurrent at same time , and the number of threads in waiting queue. It can also set waiting policies and overload strategies.

LiteGo:「迷你」的Android异步并发类库 LiteGo是一款基于Java语言的「异步并发类库」,它的核心是一枚「迷你」并发器,它可以自由地设置同一时段的最大「并发」数量,等待「排队」线程数量,还可以设置「排队策略」和「超载策略」。 LiteGo可以直接投入Runnable、Callable

马天宇 189 Nov 10, 2022
Active record style SQLite persistence for Android

ActiveAndroid ActiveAndroid is an active record style ORM (object relational mapper). What does that mean exactly? Well, ActiveAndroid allows you to s

Michael Pardo 4.7k Dec 29, 2022
Object-relational mapping for Android

RushOrm Object-relational mapping for Android RushOrm replaces the need for SQL by mapping java classes to SQL tables. What is the aim? The aim is to

Stuart Campbell 172 Nov 11, 2022
Insanely easy way to work with Android Database.

Sugar ORM Insanely easy way to work with Android databases. Official documentation can be found here - Check some examples below. The example applicat

null 2.6k Dec 16, 2022
Android ORM

Shillelagh Shillelagh is an sqlite library. It was built to make life easier. The entire library was built around simplicity when using sqlite in Andr

Andrew Reitz 49 Sep 11, 2020
Compile-time active record ORM for Android

Ollie Compile-time active record ORM for Android. Multiple mapping methods. SQLiteDatabase-like interface (QueryUtils.java). Lightweight query builder

Michael Pardo 423 Dec 30, 2022
requery - modern SQL based query & persistence for Java / Kotlin / Android

A light but powerful object mapping and SQL generator for Java/Kotlin/Android with RxJava and Java 8 support. Easily map to or create databases, perfo

requery 3.1k Dec 29, 2022
Core Data for Android

NexusData Core Data for Android NexusData is an object graph and persistence framework for Android. It allows for organizing and managing relational d

Dia Kharrat 71 Nov 11, 2022
A simple NoSQL client for Android. Meant as a document store using key/value pairs and some rudimentary querying. Useful for avoiding the hassle of SQL code.

SimpleNoSQL A simple NoSQL client for Android. If you ever wanted to just save some data but didn't really want to worry about where it was going to b

Colin Miller 389 Sep 25, 2022
Active record style SQLite persistence for Android

ActiveAndroid ActiveAndroid is an active record style ORM (object relational mapper). What does that mean exactly? Well, ActiveAndroid allows you to s

Michael Pardo 4.7k Dec 29, 2022