Introducción
Como no está realmente claro en su pregunta con qué está teniendo problemas exactamente, escribí este tutorial rápido sobre cómo implementar esta función; Si todavía tiene preguntas, no dude en preguntar.
Tengo un ejemplo práctico de todo lo que estoy hablando aquí en este repositorio de GitHub .
Si desea saber más sobre el proyecto de ejemplo, visite la página de inicio del proyecto .
En cualquier caso, el resultado debería verse así:
Si primero quieres jugar con la aplicación de demostración, puedes instalarla desde Play Store:
De todos modos, comencemos.
Configurar el SearchView
En la carpeta res/menu
cree un nuevo archivo llamado main_menu.xml
. En él, agregue un elemento y establezca el actionViewClass
en android.support.v7.widget.SearchView
. Como está utilizando la biblioteca de soporte, debe usar el espacio de nombres de la biblioteca de soporte para establecer el actionViewClass
atributo. Su archivo xml debería verse así:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_search"
android:title="@string/action_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="always"/>
</menu>
En su Fragment
o Activity
tiene que inflar este menú xml como de costumbre, luego puede buscar el MenuItem
que contiene SearchView
e implementar el OnQueryTextListener
que vamos a usar para escuchar los cambios en el texto ingresado en SearchView
:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
final MenuItem searchItem = menu.findItem(R.id.action_search);
final SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setOnQueryTextListener(this);
return true;
}
@Override
public boolean onQueryTextChange(String query) {
// Here is where we are going to implement the filter logic
return false;
}
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
Y ahora SearchView
está listo para ser utilizado. Implementaremos la lógica del filtro más adelante onQueryTextChange()
una vez que hayamos terminado de implementar el Adapter
.
Configurar el Adapter
En primer lugar, esta es la clase de modelo que voy a usar para este ejemplo:
public class ExampleModel {
private final long mId;
private final String mText;
public ExampleModel(long id, String text) {
mId = id;
mText = text;
}
public long getId() {
return mId;
}
public String getText() {
return mText;
}
}
Es solo su modelo básico que mostrará un texto en el RecyclerView
. Este es el diseño que voy a usar para mostrar el texto:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="model"
type="com.github.wrdlbrnft.searchablerecyclerviewdemo.ui.models.ExampleModel"/>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@{model.text}"/>
</FrameLayout>
</layout>
Como puede ver, uso el enlace de datos. Si nunca antes ha trabajado con el enlace de datos, ¡no se desanime! Es muy simple y poderoso, sin embargo, no puedo explicar cómo funciona en el alcance de esta respuesta.
Este es el ViewHolder
para la ExampleModel
clase:
public class ExampleViewHolder extends RecyclerView.ViewHolder {
private final ItemExampleBinding mBinding;
public ExampleViewHolder(ItemExampleBinding binding) {
super(binding.getRoot());
mBinding = binding;
}
public void bind(ExampleModel item) {
mBinding.setModel(item);
}
}
De nuevo nada especial. Solo usa el enlace de datos para vincular la clase de modelo a este diseño como hemos definido en el diseño xml anterior.
Ahora finalmente podemos llegar a la parte realmente interesante: escribir el adaptador. Voy a omitir la implementación básica deAdapter
y, en cambio, me voy a concentrar en las partes que son relevantes para esta respuesta.
Pero primero hay una cosa de la que tenemos que hablar: la SortedList
clase.
SortedList
El SortedList
es una herramienta totalmente increíble que es parte de la RecyclerView
biblioteca. Se encarga de notificar los Adapter
cambios sobre el conjunto de datos y lo hace de una manera muy eficiente. Lo único que debe hacer es especificar un orden de los elementos. Debe hacer eso implementando un compare()
método que compare dos elementos de la SortedList
misma manera que a Comparator
. Pero en lugar de ordenar un List
, se usa para ordenar los elementos en el RecyclerView
!
El SortedList
interactúa con a Adapter
través de una Callback
clase que debe implementar:
private final SortedList.Callback<ExampleModel> mCallback = new SortedList.Callback<ExampleModel>() {
@Override
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
mAdapter.notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
mAdapter.notifyItemRangeChanged(position, count);
}
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
}
En los métodos en la parte superior de la devolución de llamada como onMoved
, onInserted
, etc. usted tiene que llamar el equivalente método de su notificar Adapter
. Los tres métodos en la parte inferior compare
, areContentsTheSame
yareItemsTheSame
tiene que implementar de acuerdo con el tipo de objetos que desea mostrar y en qué orden deben aparecer estos objetos en la pantalla.
Veamos estos métodos uno por uno:
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
Este es el compare()
método del que hablé anteriormente. En este ejemplo, solo estoy pasando la llamada a una Comparator
que compara los dos modelos. Si desea que los elementos aparezcan en orden alfabético en la pantalla. Este comparador podría verse así:
private static final Comparator<ExampleModel> ALPHABETICAL_COMPARATOR = new Comparator<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return a.getText().compareTo(b.getText());
}
};
Ahora echemos un vistazo al siguiente método:
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
El propósito de este método es determinar si el contenido de un modelo ha cambiado. Esto lo SortedList
utiliza para determinar si es necesario invocar un evento de cambio, en otras palabras, si el RecyclerView
fundido cruza la versión antigua y la nueva. Si ustedes, las clases tienen un modelo correcto equals()
y hashCode()
la aplicación se puede aplicar por lo general sólo se siente más arriba. Si agregamos una equals()
e hashCode()
implementación a la ExampleModel
clase, debería verse así:
public class ExampleModel implements SortedListAdapter.ViewModel {
private final long mId;
private final String mText;
public ExampleModel(long id, String text) {
mId = id;
mText = text;
}
public long getId() {
return mId;
}
public String getText() {
return mText;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExampleModel model = (ExampleModel) o;
if (mId != model.mId) return false;
return mText != null ? mText.equals(model.mText) : model.mText == null;
}
@Override
public int hashCode() {
int result = (int) (mId ^ (mId >>> 32));
result = 31 * result + (mText != null ? mText.hashCode() : 0);
return result;
}
}
Nota al margen: ¡La mayoría de los IDE como Android Studio, IntelliJ y Eclipse tienen funcionalidad para generar equals()
e hashCode()
implementaciones para usted con solo presionar un botón! Por lo tanto, no tiene que implementarlos usted mismo. ¡Mira en Internet cómo funciona en tu IDE!
Ahora echemos un vistazo al último método:
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
El SortedList
utiliza este método para comprobar si dos artículos se refieren a la misma cosa. En términos más simples (sin explicar cómo SortedList
funciona) esto se usa para determinar si un objeto ya está contenido en elList
animación y si es necesario agregar, mover o cambiar la animación. Si sus modelos tienen una identificación, generalmente compararía solo la identificación en este método. Si no lo hacen, necesita encontrar otra forma de verificar esto, pero, sin embargo, termina implementando esto dependiendo de su aplicación específica. Por lo general, es la opción más simple para dar a todos los modelos una identificación, que podría ser, por ejemplo, el campo de clave principal si está consultando los datos de una base de datos.
Con la SortedList.Callback
implementación correcta, podemos crear una instancia de SortedList
:
final SortedList<ExampleModel> list = new SortedList<>(ExampleModel.class, mCallback);
Como primer parámetro en el constructor del SortedList
, debe pasar la clase de sus modelos. El otro parámetro es solo el SortedList.Callback
que definimos anteriormente.
Ahora pasemos a los negocios: si implementamos el Adapter
con un SortedList
debería ser algo como esto:
public class ExampleAdapter extends RecyclerView.Adapter<ExampleViewHolder> {
private final SortedList<ExampleModel> mSortedList = new SortedList<>(ExampleModel.class, new SortedList.Callback<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
@Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
notifyItemRangeChanged(position, count);
}
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
});
private final LayoutInflater mInflater;
private final Comparator<ExampleModel> mComparator;
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
mInflater = LayoutInflater.from(context);
mComparator = comparator;
}
@Override
public ExampleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(inflater, parent, false);
return new ExampleViewHolder(binding);
}
@Override
public void onBindViewHolder(ExampleViewHolder holder, int position) {
final ExampleModel model = mSortedList.get(position);
holder.bind(model);
}
@Override
public int getItemCount() {
return mSortedList.size();
}
}
Lo que se Comparator
usa para ordenar el elemento se pasa a través del constructor para que podamos usar el mismo Adapter
incluso si se supone que los elementos se muestran en un orden diferente.
¡Ya casi hemos terminado! Pero primero necesitamos una forma de agregar o eliminar elementos al Adapter
. Para este propósito, podemos agregar métodos Adapter
que nos permiten agregar y eliminar elementos a SortedList
:
public void add(ExampleModel model) {
mSortedList.add(model);
}
public void remove(ExampleModel model) {
mSortedList.remove(model);
}
public void add(List<ExampleModel> models) {
mSortedList.addAll(models);
}
public void remove(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (ExampleModel model : models) {
mSortedList.remove(model);
}
mSortedList.endBatchedUpdates();
}
¡No necesitamos llamar a ningún método de notificación aquí porque SortedList
ya lo hace a través de SortedList.Callback
! Aparte de eso, la implementación de estos métodos es bastante sencilla, con una excepción: el método remove que elimina algunos List
de los modelos. Dado que SortedList
solo tiene un método de eliminación que puede eliminar un solo objeto, necesitamos recorrer la lista y eliminar los modelos uno por uno. Llamar beginBatchedUpdates()
al principio agrupa todos los cambios que haremos SortedList
juntos y mejora el rendimiento. Cuando llamamos a endBatchedUpdates()
la RecyclerView
que se notifique acerca de todos los cambios a la vez.
Además, lo que debe comprender es que si agrega un objeto al SortedList
y ya está en el SortedList
, no se agregará nuevamente. En su lugar, SortedList
utiliza el areContentsTheSame()
método para determinar si el objeto ha cambiado, y si tiene el elemento RecyclerView
, se actualizará.
De todos modos, lo que generalmente prefiero es un método que me permite reemplazar todos los elementos a la RecyclerView
vez. Elimine todo lo que no esté en el List
y agregue todos los elementos que faltan en SortedList
:
public void replaceAll(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (int i = mSortedList.size() - 1; i >= 0; i--) {
final ExampleModel model = mSortedList.get(i);
if (!models.contains(model)) {
mSortedList.remove(model);
}
}
mSortedList.addAll(models);
mSortedList.endBatchedUpdates();
}
Este método vuelve a agrupar todas las actualizaciones para aumentar el rendimiento. El primer bucle es inverso, ya que eliminar un elemento al principio estropearía los índices de todos los elementos que aparecen después y esto puede conducir en algunos casos a problemas como inconsistencias de datos. Después de eso, simplemente agregamos el List
al SortedList
uso addAll()
para agregar todos los elementos que aún no están en el SortedList
y, tal como describí anteriormente, actualizar todos los elementos que ya están en el SortedList
pero que han cambiado.
Y con eso Adapter
se completa. Todo debería verse así:
public class ExampleAdapter extends RecyclerView.Adapter<ExampleViewHolder> {
private final SortedList<ExampleModel> mSortedList = new SortedList<>(ExampleModel.class, new SortedList.Callback<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
@Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
notifyItemRangeChanged(position, count);
}
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1 == item2;
}
});
private final Comparator<ExampleModel> mComparator;
private final LayoutInflater mInflater;
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
mInflater = LayoutInflater.from(context);
mComparator = comparator;
}
@Override
public ExampleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(mInflater, parent, false);
return new ExampleViewHolder(binding);
}
@Override
public void onBindViewHolder(ExampleViewHolder holder, int position) {
final ExampleModel model = mSortedList.get(position);
holder.bind(model);
}
public void add(ExampleModel model) {
mSortedList.add(model);
}
public void remove(ExampleModel model) {
mSortedList.remove(model);
}
public void add(List<ExampleModel> models) {
mSortedList.addAll(models);
}
public void remove(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (ExampleModel model : models) {
mSortedList.remove(model);
}
mSortedList.endBatchedUpdates();
}
public void replaceAll(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (int i = mSortedList.size() - 1; i >= 0; i--) {
final ExampleModel model = mSortedList.get(i);
if (!models.contains(model)) {
mSortedList.remove(model);
}
}
mSortedList.addAll(models);
mSortedList.endBatchedUpdates();
}
@Override
public int getItemCount() {
return mSortedList.size();
}
}
¡Lo único que falta ahora es implementar el filtrado!
Implementando la lógica del filtro
Para implementar la lógica del filtro, primero tenemos que definir uno List
de todos los modelos posibles. Para este ejemplo se crea una List
de ExampleModel
las instancias de una serie de películas:
private static final String[] MOVIES = new String[]{
...
};
private static final Comparator<ExampleModel> ALPHABETICAL_COMPARATOR = new Comparator<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return a.getText().compareTo(b.getText());
}
};
private ExampleAdapter mAdapter;
private List<ExampleModel> mModels;
private RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mAdapter = new ExampleAdapter(this, ALPHABETICAL_COMPARATOR);
mBinding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
mBinding.recyclerView.setAdapter(mAdapter);
mModels = new ArrayList<>();
for (String movie : MOVIES) {
mModels.add(new ExampleModel(movie));
}
mAdapter.add(mModels);
}
No pasa nada especial aquí, solo instanciamos el Adapter
y lo configuramos en RecyclerView
. Después de eso creamos una List
de modelos a partir de los nombres de las películas en la MOVIES
matriz. Luego agregamos todos los modelos al SortedList
.
Ahora podemos volver a onQueryTextChange()
lo que definimos anteriormente y comenzar a implementar la lógica de filtro:
@Override
public boolean onQueryTextChange(String query) {
final List<ExampleModel> filteredModelList = filter(mModels, query);
mAdapter.replaceAll(filteredModelList);
mBinding.recyclerView.scrollToPosition(0);
return true;
}
Esto es nuevamente bastante sencillo. Llamamos al método filter()
y el pase en el List
de ExampleModel
s, así como la cadena de consulta. Luego llamamos replaceAll()
al Adapter
y pasamos el filtrado List
devuelto por filter()
. También tenemos que llamar scrollToPosition(0)
al RecyclerView
asegurar que el usuario siempre puede ver todos los elementos en la búsqueda de algo. De lo contrario, RecyclerView
podría permanecer en una posición desplazada hacia abajo mientras se filtra y posteriormente ocultar algunos elementos. Desplazarse hacia arriba asegura una mejor experiencia de usuario mientras busca.
Lo único que queda por hacer ahora es implementarse filter()
:
private static List<ExampleModel> filter(List<ExampleModel> models, String query) {
final String lowerCaseQuery = query.toLowerCase();
final List<ExampleModel> filteredModelList = new ArrayList<>();
for (ExampleModel model : models) {
final String text = model.getText().toLowerCase();
if (text.contains(lowerCaseQuery)) {
filteredModelList.add(model);
}
}
return filteredModelList;
}
Lo primero que hacemos aquí es llamar toLowerCase()
a la cadena de consulta. No queremos que nuestra función de búsqueda distinga entre mayúsculas y minúsculas y, al invocar toLowerCase()
todas las cadenas que comparamos, podemos asegurarnos de que arrojemos los mismos resultados independientemente del caso. Luego solo itera a través de todos los modelos List
que pasamos y verifica si la cadena de consulta está contenida en el texto del modelo. Si es así, el modelo se agrega al filtrado List
.
¡Y eso es! El código anterior se ejecutará en el nivel de API 7 y superior y, a partir del nivel de API 11, ¡obtendrá animaciones de elementos de forma gratuita!
Soy consciente de que esta es una descripción muy detallada que probablemente hace que todo esto parezca más complicado de lo que realmente es, pero hay una manera que podemos generalizar todo este problema y hacer que la implementación de una Adapter
base a una SortedList
mucho más simple.
Generalizando el problema y simplificando el adaptador
En esta sección no voy a entrar en muchos detalles, en parte porque me estoy enfrentando al límite de caracteres para las respuestas en Stack Overflow, pero también porque la mayoría ya se explicó anteriormente, pero para resumir los cambios: podemos implementar una Adapter
clase base que ya se encarga de tratar SortedList
tanto los modelos vinculantes como los de ViewHolder
instancias y proporciona una forma conveniente de implementar un Adapter
basado en a SortedList
. Para eso tenemos que hacer dos cosas:
- Necesitamos crear una
ViewModel
interfaz que todas las clases de modelos tengan que implementar
- Necesitamos crear una
ViewHolder
subclase que defina un bind()
método que Adapter
pueda usar para vincular modelos automáticamente.
Esto nos permite centrarnos solo en el contenido que se supone que se muestra en la RecyclerView
implementación de los modelos y las ViewHolder
implementaciones correspondientes . Al usar esta clase base, no tenemos que preocuparnos por los intrincados detalles del Adapter
y sus SortedList
.
SortedListAdapter
Debido al límite de caracteres para las respuestas en StackOverflow, no puedo seguir cada paso de la implementación de esta clase base o incluso agregar el código fuente completo aquí, pero puedes encontrar el código fuente completo de esta clase base, lo llamé SortedListAdapter
, en este GitHub Gist .
Para simplificar su vida, he publicado una biblioteca en jCenter que contiene el SortedListAdapter
! Si desea usarlo, todo lo que necesita hacer es agregar esta dependencia al archivo build.gradle de su aplicación:
compile 'com.github.wrdlbrnft:sorted-list-adapter:0.2.0.1'
Puede encontrar más información sobre esta biblioteca en la página de inicio de la biblioteca .
Usando el SortedListAdapter
Para usar el SortedListAdapter
tenemos que hacer dos cambios:
Cambia el ViewHolder
para que se extienda SortedListAdapter.ViewHolder
. El parámetro tipo debería ser el modelo que debería estar vinculado a esto ViewHolder
, en este caso ExampleModel
. Debe vincular datos a sus modelos en performBind()
lugar de bind()
.
public class ExampleViewHolder extends SortedListAdapter.ViewHolder<ExampleModel> {
private final ItemExampleBinding mBinding;
public ExampleViewHolder(ItemExampleBinding binding) {
super(binding.getRoot());
mBinding = binding;
}
@Override
protected void performBind(ExampleModel item) {
mBinding.setModel(item);
}
}
Asegúrese de que todos sus modelos implementen la ViewModel
interfaz:
public class ExampleModel implements SortedListAdapter.ViewModel {
...
}
Después de eso, solo tenemos que actualizar ExampleAdapter
para ampliar SortedListAdapter
y eliminar todo lo que ya no necesitamos. El parámetro de tipo debe ser el tipo de modelo con el que está trabajando, en este caso ExampleModel
. Pero si está trabajando con diferentes tipos de modelos, configure el parámetro de tipo en ViewModel
.
public class ExampleAdapter extends SortedListAdapter<ExampleModel> {
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
super(context, ExampleModel.class, comparator);
}
@Override
protected ViewHolder<? extends ExampleModel> onCreateViewHolder(LayoutInflater inflater, ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(inflater, parent, false);
return new ExampleViewHolder(binding);
}
@Override
protected boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
@Override
protected boolean areItemContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
}
Después de eso hemos terminado! Sin embargo, una última cosa para mencionar: el SortedListAdapter
no tiene el mismo add()
, remove()
o los replaceAll()
métodos que ExampleAdapter
tenía nuestro original . Utiliza un Editor
objeto separado para modificar los elementos de la lista a los que se puede acceder a través del edit()
método. Entonces, si desea eliminar o agregar elementos a los que tiene que llamar edit()
, agregue y elimine los elementos en esta Editor
instancia y una vez que haya terminado, invoque commit()
para aplicar los cambios a SortedList
:
mAdapter.edit()
.remove(modelToRemove)
.add(listOfModelsToAdd)
.commit();
Todos los cambios que realice de esta manera se agrupan para aumentar el rendimiento. El replaceAll()
método que implementamos en los capítulos anteriores también está presente en este Editor
objeto:
mAdapter.edit()
.replaceAll(mModels)
.commit();
Si olvida llamar commit()
, ¡ninguno de sus cambios se aplicará!